Spaces:
Running
Running
Fan-out workflow: describe + extract JSON details in parallel, chain into story writer
Browse files- run.py +67 -21
- workflow.json +110 -83
run.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import base64
|
|
|
|
| 2 |
import mimetypes
|
| 3 |
import os
|
| 4 |
|
|
@@ -6,6 +7,8 @@ import gradio as gr
|
|
| 6 |
from huggingface_hub import get_token
|
| 7 |
from openai import OpenAI
|
| 8 |
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def _to_data_url(image) -> str:
|
| 11 |
"""Workflow image ports arrive as FileData-like dicts, e.g.
|
|
@@ -29,32 +32,21 @@ def _to_data_url(image) -> str:
|
|
| 29 |
raise ValueError(f"Unsupported image value: {str(image)[:200]}")
|
| 30 |
|
| 31 |
|
| 32 |
-
def
|
| 33 |
-
|
| 34 |
-
) -> str:
|
| 35 |
-
"""Describe an uploaded image with DeepSeek-V4.1-Flash.
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
`huggingface_hub login` token. It never appears as a canvas port.
|
| 40 |
"""
|
| 41 |
api_key = token.token if token else get_token()
|
| 42 |
-
client = OpenAI(
|
| 43 |
-
|
| 44 |
-
api_key=api_key,
|
| 45 |
-
)
|
| 46 |
stream = client.chat.completions.create(
|
| 47 |
-
model=
|
| 48 |
-
messages=[
|
| 49 |
-
{
|
| 50 |
-
"role": "user",
|
| 51 |
-
"content": [
|
| 52 |
-
{"type": "text", "text": prompt},
|
| 53 |
-
{"type": "image_url", "image_url": {"url": _to_data_url(image)}},
|
| 54 |
-
],
|
| 55 |
-
}
|
| 56 |
-
],
|
| 57 |
stream=True,
|
|
|
|
| 58 |
)
|
| 59 |
output = ""
|
| 60 |
for chunk in stream:
|
|
@@ -63,7 +55,61 @@ def describe_image(
|
|
| 63 |
return output
|
| 64 |
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
gr.Workflow(
|
| 67 |
-
bind={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
graph="workflow.json",
|
| 69 |
).launch()
|
|
|
|
| 1 |
import base64
|
| 2 |
+
import json
|
| 3 |
import mimetypes
|
| 4 |
import os
|
| 5 |
|
|
|
|
| 7 |
from huggingface_hub import get_token
|
| 8 |
from openai import OpenAI
|
| 9 |
|
| 10 |
+
MODEL = "deepseek-ai/DeepSeek-V4.1-Flash:novita"
|
| 11 |
+
|
| 12 |
|
| 13 |
def _to_data_url(image) -> str:
|
| 14 |
"""Workflow image ports arrive as FileData-like dicts, e.g.
|
|
|
|
| 32 |
raise ValueError(f"Unsupported image value: {str(image)[:200]}")
|
| 33 |
|
| 34 |
|
| 35 |
+
def _chat(content: list, token: "gr.OAuthToken | None", json_mode: bool = False) -> str:
|
| 36 |
+
"""One chat completion against DeepSeek-V4.1-Flash via the HF router.
|
|
|
|
|
|
|
| 37 |
|
| 38 |
+
`token` is injected by gr.Workflow per request: on Spaces it is the
|
| 39 |
+
visitor's OAuth token; locally it falls back to the host's
|
| 40 |
`huggingface_hub login` token. It never appears as a canvas port.
|
| 41 |
"""
|
| 42 |
api_key = token.token if token else get_token()
|
| 43 |
+
client = OpenAI(base_url="https://router.huggingface.co/v1", api_key=api_key)
|
| 44 |
+
kwargs = {"response_format": {"type": "json_object"}} if json_mode else {}
|
|
|
|
|
|
|
| 45 |
stream = client.chat.completions.create(
|
| 46 |
+
model=MODEL,
|
| 47 |
+
messages=[{"role": "user", "content": content}],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
stream=True,
|
| 49 |
+
**kwargs,
|
| 50 |
)
|
| 51 |
output = ""
|
| 52 |
for chunk in stream:
|
|
|
|
| 55 |
return output
|
| 56 |
|
| 57 |
|
| 58 |
+
def describe_image(
|
| 59 |
+
image: dict, prompt: str, token: gr.OAuthToken | None = None
|
| 60 |
+
) -> str:
|
| 61 |
+
"""Describe an uploaded image with DeepSeek-V4.1-Flash."""
|
| 62 |
+
return _chat(
|
| 63 |
+
[
|
| 64 |
+
{"type": "text", "text": prompt},
|
| 65 |
+
{"type": "image_url", "image_url": {"url": _to_data_url(image)}},
|
| 66 |
+
],
|
| 67 |
+
token,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def extract_details(image: dict, token: gr.OAuthToken | None = None) -> dict:
|
| 72 |
+
"""Extract structured details from the image as JSON."""
|
| 73 |
+
text = _chat(
|
| 74 |
+
[
|
| 75 |
+
{
|
| 76 |
+
"type": "text",
|
| 77 |
+
"text": (
|
| 78 |
+
"Analyze this image and respond with a JSON object with keys: "
|
| 79 |
+
'"subject" (main subject), "setting" (where it is), '
|
| 80 |
+
'"colors" (list of dominant colors), "mood" (one word), '
|
| 81 |
+
'"objects" (list of notable objects).'
|
| 82 |
+
),
|
| 83 |
+
},
|
| 84 |
+
{"type": "image_url", "image_url": {"url": _to_data_url(image)}},
|
| 85 |
+
],
|
| 86 |
+
token,
|
| 87 |
+
json_mode=True,
|
| 88 |
+
)
|
| 89 |
+
return json.loads(text)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def write_story(description: str, token: gr.OAuthToken | None = None) -> str:
|
| 93 |
+
"""Write a short story based on an image description."""
|
| 94 |
+
return _chat(
|
| 95 |
+
[
|
| 96 |
+
{
|
| 97 |
+
"type": "text",
|
| 98 |
+
"text": (
|
| 99 |
+
"Write a short, vivid 3-sentence story inspired by this "
|
| 100 |
+
f"image description:\n\n{description}"
|
| 101 |
+
),
|
| 102 |
+
}
|
| 103 |
+
],
|
| 104 |
+
token,
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
gr.Workflow(
|
| 109 |
+
bind={
|
| 110 |
+
"Describe Image": describe_image,
|
| 111 |
+
"Extract Details": extract_details,
|
| 112 |
+
"Write Story": write_story,
|
| 113 |
+
},
|
| 114 |
graph="workflow.json",
|
| 115 |
).launch()
|
workflow.json
CHANGED
|
@@ -1,50 +1,30 @@
|
|
| 1 |
{
|
| 2 |
"schema_version": "2",
|
| 3 |
-
"name": "DeepSeek Image
|
| 4 |
"references": [
|
| 5 |
{
|
| 6 |
"id": "ref_image",
|
| 7 |
"label": "Upload Image",
|
| 8 |
"role": "reference",
|
| 9 |
"asset_type": "image",
|
| 10 |
-
"
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
],
|
| 17 |
-
"outputs": [
|
| 18 |
-
{
|
| 19 |
-
"id": "out",
|
| 20 |
-
"label": "Image",
|
| 21 |
-
"type": "image"
|
| 22 |
-
}
|
| 23 |
-
],
|
| 24 |
-
"data": {},
|
| 25 |
-
"width": 260
|
| 26 |
},
|
| 27 |
{
|
| 28 |
"id": "ref_prompt",
|
| 29 |
"label": "Prompt",
|
| 30 |
"role": "reference",
|
| 31 |
"asset_type": "text",
|
| 32 |
-
"
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
],
|
| 39 |
-
"outputs": [
|
| 40 |
-
{
|
| 41 |
-
"id": "out",
|
| 42 |
-
"label": "Text",
|
| 43 |
-
"type": "text"
|
| 44 |
-
}
|
| 45 |
-
],
|
| 46 |
-
"data": {},
|
| 47 |
-
"width": 260
|
| 48 |
}
|
| 49 |
],
|
| 50 |
"operators": [
|
|
@@ -54,30 +34,45 @@
|
|
| 54 |
"role": "operator",
|
| 55 |
"kind": "fn",
|
| 56 |
"fn": "Describe Image",
|
|
|
|
|
|
|
|
|
|
| 57 |
"inputs": [
|
| 58 |
-
{
|
| 59 |
-
|
| 60 |
-
"label": "image",
|
| 61 |
-
"type": "image",
|
| 62 |
-
"required": true
|
| 63 |
-
},
|
| 64 |
-
{
|
| 65 |
-
"id": "in_prompt",
|
| 66 |
-
"label": "prompt",
|
| 67 |
-
"type": "text",
|
| 68 |
-
"required": true
|
| 69 |
-
}
|
| 70 |
],
|
| 71 |
-
"outputs": [
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
],
|
| 79 |
-
"
|
| 80 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
],
|
| 83 |
"subjects": [
|
|
@@ -86,48 +81,80 @@
|
|
| 86 |
"label": "Description",
|
| 87 |
"role": "subject",
|
| 88 |
"asset_type": "text",
|
| 89 |
-
"
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
"
|
| 104 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
}
|
| 106 |
],
|
| 107 |
"edges": [
|
| 108 |
{
|
| 109 |
"id": "e1",
|
| 110 |
-
"from_node_id": "ref_image",
|
| 111 |
-
"
|
| 112 |
-
"to_node_id": "op_describe",
|
| 113 |
-
"to_port_id": "in_image",
|
| 114 |
"type": "image"
|
| 115 |
},
|
| 116 |
{
|
| 117 |
"id": "e2",
|
| 118 |
-
"from_node_id": "ref_prompt",
|
| 119 |
-
"
|
| 120 |
-
"to_node_id": "op_describe",
|
| 121 |
-
"to_port_id": "in_prompt",
|
| 122 |
"type": "text"
|
| 123 |
},
|
| 124 |
{
|
| 125 |
"id": "e3",
|
| 126 |
-
"from_node_id": "
|
| 127 |
-
"
|
| 128 |
-
"
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
"type": "text"
|
| 131 |
}
|
| 132 |
]
|
| 133 |
-
}
|
|
|
|
| 1 |
{
|
| 2 |
"schema_version": "2",
|
| 3 |
+
"name": "DeepSeek Image Studio",
|
| 4 |
"references": [
|
| 5 |
{
|
| 6 |
"id": "ref_image",
|
| 7 |
"label": "Upload Image",
|
| 8 |
"role": "reference",
|
| 9 |
"asset_type": "image",
|
| 10 |
+
"x": 40,
|
| 11 |
+
"y": 180,
|
| 12 |
+
"width": 260,
|
| 13 |
+
"inputs": [{"id": "in", "label": "Image", "type": "image"}],
|
| 14 |
+
"outputs": [{"id": "out", "label": "Image", "type": "image"}],
|
| 15 |
+
"data": {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
},
|
| 17 |
{
|
| 18 |
"id": "ref_prompt",
|
| 19 |
"label": "Prompt",
|
| 20 |
"role": "reference",
|
| 21 |
"asset_type": "text",
|
| 22 |
+
"x": 40,
|
| 23 |
+
"y": 520,
|
| 24 |
+
"width": 260,
|
| 25 |
+
"inputs": [{"id": "in", "label": "Text", "type": "text"}],
|
| 26 |
+
"outputs": [{"id": "out", "label": "Text", "type": "text"}],
|
| 27 |
+
"data": {"out": "Describe this image in one sentence."}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
],
|
| 30 |
"operators": [
|
|
|
|
| 34 |
"role": "operator",
|
| 35 |
"kind": "fn",
|
| 36 |
"fn": "Describe Image",
|
| 37 |
+
"x": 460,
|
| 38 |
+
"y": 80,
|
| 39 |
+
"width": 300,
|
| 40 |
"inputs": [
|
| 41 |
+
{"id": "in_image", "label": "image", "type": "image", "required": true},
|
| 42 |
+
{"id": "in_prompt", "label": "prompt", "type": "text", "required": true}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
],
|
| 44 |
+
"outputs": [{"id": "out_0", "label": "output", "type": "text", "output_index": 0}],
|
| 45 |
+
"data": {}
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"id": "op_extract",
|
| 49 |
+
"label": "Extract Details",
|
| 50 |
+
"role": "operator",
|
| 51 |
+
"kind": "fn",
|
| 52 |
+
"fn": "Extract Details",
|
| 53 |
+
"x": 460,
|
| 54 |
+
"y": 420,
|
| 55 |
+
"width": 300,
|
| 56 |
+
"inputs": [
|
| 57 |
+
{"id": "in_image", "label": "image", "type": "image", "required": true}
|
| 58 |
],
|
| 59 |
+
"outputs": [{"id": "out_0", "label": "output", "type": "json", "output_index": 0}],
|
| 60 |
+
"data": {}
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"id": "op_story",
|
| 64 |
+
"label": "Write Story",
|
| 65 |
+
"role": "operator",
|
| 66 |
+
"kind": "fn",
|
| 67 |
+
"fn": "Write Story",
|
| 68 |
+
"x": 920,
|
| 69 |
+
"y": 80,
|
| 70 |
+
"width": 300,
|
| 71 |
+
"inputs": [
|
| 72 |
+
{"id": "in_description", "label": "description", "type": "text", "required": true}
|
| 73 |
+
],
|
| 74 |
+
"outputs": [{"id": "out_0", "label": "output", "type": "text", "output_index": 0}],
|
| 75 |
+
"data": {}
|
| 76 |
}
|
| 77 |
],
|
| 78 |
"subjects": [
|
|
|
|
| 81 |
"label": "Description",
|
| 82 |
"role": "subject",
|
| 83 |
"asset_type": "text",
|
| 84 |
+
"x": 1380,
|
| 85 |
+
"y": 40,
|
| 86 |
+
"width": 320,
|
| 87 |
+
"inputs": [{"id": "in", "label": "Text", "type": "text"}],
|
| 88 |
+
"outputs": [{"id": "out", "label": "Text", "type": "text"}],
|
| 89 |
+
"data": {}
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"id": "sub_details",
|
| 93 |
+
"label": "Details",
|
| 94 |
+
"role": "subject",
|
| 95 |
+
"asset_type": "json",
|
| 96 |
+
"x": 920,
|
| 97 |
+
"y": 420,
|
| 98 |
+
"width": 320,
|
| 99 |
+
"inputs": [{"id": "in", "label": "JSON", "type": "json"}],
|
| 100 |
+
"outputs": [{"id": "out", "label": "JSON", "type": "json"}],
|
| 101 |
+
"data": {}
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"id": "sub_story",
|
| 105 |
+
"label": "Story",
|
| 106 |
+
"role": "subject",
|
| 107 |
+
"asset_type": "text",
|
| 108 |
+
"x": 1380,
|
| 109 |
+
"y": 300,
|
| 110 |
+
"width": 320,
|
| 111 |
+
"inputs": [{"id": "in", "label": "Text", "type": "text"}],
|
| 112 |
+
"outputs": [{"id": "out", "label": "Text", "type": "text"}],
|
| 113 |
+
"data": {}
|
| 114 |
}
|
| 115 |
],
|
| 116 |
"edges": [
|
| 117 |
{
|
| 118 |
"id": "e1",
|
| 119 |
+
"from_node_id": "ref_image", "from_port_id": "out",
|
| 120 |
+
"to_node_id": "op_describe", "to_port_id": "in_image",
|
|
|
|
|
|
|
| 121 |
"type": "image"
|
| 122 |
},
|
| 123 |
{
|
| 124 |
"id": "e2",
|
| 125 |
+
"from_node_id": "ref_prompt", "from_port_id": "out",
|
| 126 |
+
"to_node_id": "op_describe", "to_port_id": "in_prompt",
|
|
|
|
|
|
|
| 127 |
"type": "text"
|
| 128 |
},
|
| 129 |
{
|
| 130 |
"id": "e3",
|
| 131 |
+
"from_node_id": "ref_image", "from_port_id": "out",
|
| 132 |
+
"to_node_id": "op_extract", "to_port_id": "in_image",
|
| 133 |
+
"type": "image"
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"id": "e4",
|
| 137 |
+
"from_node_id": "op_describe", "from_port_id": "out_0",
|
| 138 |
+
"to_node_id": "op_story", "to_port_id": "in_description",
|
| 139 |
+
"type": "text"
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"id": "e5",
|
| 143 |
+
"from_node_id": "op_describe", "from_port_id": "out_0",
|
| 144 |
+
"to_node_id": "sub_description", "to_port_id": "in",
|
| 145 |
+
"type": "text"
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"id": "e6",
|
| 149 |
+
"from_node_id": "op_extract", "from_port_id": "out_0",
|
| 150 |
+
"to_node_id": "sub_details", "to_port_id": "in",
|
| 151 |
+
"type": "json"
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"id": "e7",
|
| 155 |
+
"from_node_id": "op_story", "from_port_id": "out_0",
|
| 156 |
+
"to_node_id": "sub_story", "to_port_id": "in",
|
| 157 |
"type": "text"
|
| 158 |
}
|
| 159 |
]
|
| 160 |
+
}
|