akhaliq HF Staff commited on
Commit
b67cb9d
·
1 Parent(s): f46c173

Fan-out workflow: describe + extract JSON details in parallel, chain into story writer

Browse files
Files changed (2) hide show
  1. run.py +67 -21
  2. 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 describe_image(
33
- image: dict, prompt: str, token: gr.OAuthToken | None = None
34
- ) -> str:
35
- """Describe an uploaded image with DeepSeek-V4.1-Flash.
36
 
37
- The `token` parameter is injected by gr.Workflow per request: on Spaces it
38
- is the visitor's OAuth token; locally it falls back to the host's
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
- base_url="https://router.huggingface.co/v1",
44
- api_key=api_key,
45
- )
46
  stream = client.chat.completions.create(
47
- model="deepseek-ai/DeepSeek-V4.1-Flash:novita",
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={"Describe Image": describe_image},
 
 
 
 
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 Describer",
4
  "references": [
5
  {
6
  "id": "ref_image",
7
  "label": "Upload Image",
8
  "role": "reference",
9
  "asset_type": "image",
10
- "inputs": [
11
- {
12
- "id": "in",
13
- "label": "Image",
14
- "type": "image"
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
- "inputs": [
33
- {
34
- "id": "in",
35
- "label": "Text",
36
- "type": "text"
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
- "id": "in_image",
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
- "id": "out_0",
74
- "label": "output",
75
- "type": "text",
76
- "output_index": 0
77
- }
 
 
 
 
 
 
 
78
  ],
79
- "data": {},
80
- "width": 300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
  ],
83
  "subjects": [
@@ -86,48 +81,80 @@
86
  "label": "Description",
87
  "role": "subject",
88
  "asset_type": "text",
89
- "inputs": [
90
- {
91
- "id": "in",
92
- "label": "Text",
93
- "type": "text"
94
- }
95
- ],
96
- "outputs": [
97
- {
98
- "id": "out",
99
- "label": "Text",
100
- "type": "text"
101
- }
102
- ],
103
- "data": {},
104
- "width": 320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
  ],
107
  "edges": [
108
  {
109
  "id": "e1",
110
- "from_node_id": "ref_image",
111
- "from_port_id": "out",
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
- "from_port_id": "out",
120
- "to_node_id": "op_describe",
121
- "to_port_id": "in_prompt",
122
  "type": "text"
123
  },
124
  {
125
  "id": "e3",
126
- "from_node_id": "op_describe",
127
- "from_port_id": "out_0",
128
- "to_node_id": "sub_description",
129
- "to_port_id": "in",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }