tlam commited on
Commit
3a320b8
·
1 Parent(s): 6364f3e

Refactor: rewrite ComfyUI parsing with graph tracing, add LoRA/dimensions/strip metadata, update for Gradio 6.x

Browse files
Files changed (4) hide show
  1. .gitignore +2 -0
  2. README.md +1 -1
  3. app.py +350 -119
  4. requirements.txt +2 -1
.gitignore CHANGED
@@ -1,2 +1,4 @@
1
  /venv
 
 
2
  .DS_Store
 
1
  /venv
2
+ venv/
3
+ __pycache__/
4
  .DS_Store
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🐨
4
  colorFrom: blue
5
  colorTo: blue
6
  sdk: gradio
7
- sdk_version: 4.44.0
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
 
4
  colorFrom: blue
5
  colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 5.34.0
8
  app_file: app.py
9
  pinned: false
10
  license: apache-2.0
app.py CHANGED
@@ -1,18 +1,45 @@
1
  import gradio as gr
2
  from PIL import Image
3
  from PIL.ExifTags import TAGS
4
- import numpy as np
5
  import re
6
  import json
7
  import tempfile
8
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  def get_image_metadata(img):
11
  """Extract metadata based on image format."""
12
  metadata = {}
13
- if img.format == 'PNG':
14
  metadata = img.info
15
- elif img.format in ['JPEG', 'TIFF']:
16
  exif_data = img._getexif()
17
  if exif_data:
18
  for tag, value in exif_data.items():
@@ -20,180 +47,384 @@ def get_image_metadata(img):
20
  metadata[tag_name] = value
21
  return metadata
22
 
23
- def parse_parameters(parameters):
24
- """Parse the 'parameters' field to extract prompts and seed number."""
25
- prompt = 'N/A'
26
- negative_prompt = 'N/A'
27
- adetailer_prompt_1 = 'N/A'
28
- adetailer_prompt_2 = 'N/A'
29
- seed_number = -1
30
-
31
- # Extract the prompt
32
- prompt_match = re.search(r'(.*?)Negative prompt:', parameters, re.DOTALL | re.IGNORECASE)
33
- if prompt_match:
34
- prompt = prompt_match.group(1).strip()
35
- else:
36
- prompt_match = re.search(r'(.*?)(Steps:|$)', parameters, re.DOTALL | re.IGNORECASE)
37
- if prompt_match:
38
- prompt = prompt_match.group(1).strip()
39
 
40
- # Extract the negative prompt
41
- negative_prompt_match = re.search(r'Negative prompt:(.*?)(Steps:|$)', parameters, re.DOTALL | re.IGNORECASE)
42
- if negative_prompt_match:
43
- negative_prompt = negative_prompt_match.group(1).strip()
 
44
 
45
- # Extract ADetailer prompts
46
- adetailer_prompt_1_match = re.search(r'ADetailer prompt:\s*"(.*?)"', parameters, re.IGNORECASE)
47
- if adetailer_prompt_1_match:
48
- adetailer_prompt_1 = adetailer_prompt_1_match.group(1).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- adetailer_prompt_2_match = re.search(r'ADetailer negative prompt 2nd:\s*"(.*?)"', parameters, re.IGNORECASE)
51
- if adetailer_prompt_2_match:
52
- adetailer_prompt_2 = adetailer_prompt_2_match.group(1).strip()
 
 
 
 
 
53
 
54
- # Extract Seed Number
55
- seed_match = re.search(r'Seed:\s*(\d+)', parameters, re.IGNORECASE)
56
- if seed_match:
57
- seed_number = seed_match.group(1).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- return prompt, negative_prompt, adetailer_prompt_1, adetailer_prompt_2, seed_number
60
 
61
  def extract_metadata(image_file):
62
  """Extract and parse metadata from the uploaded image."""
63
  try:
64
  img = Image.open(image_file)
65
- except Exception as e:
66
- # Return placeholders in case of error
67
- return (None, 'Error: Unable to open image.', *(['N/A'] * 7))
68
 
69
  metadata = get_image_metadata(img)
70
- img_display = np.array(img)
 
 
71
 
72
- # Convert metadata to string
73
  metadata_str = "\n".join([f"{key}: {value}" for key, value in metadata.items()])
74
 
75
- # Initialize default values
76
- prompt = negative_prompt = adetailer_prompt_1 = adetailer_prompt_2 = 'N/A'
77
- seed_number = -1
78
- comfy_prompt = comfy_workflow = 'N/A'
 
79
 
80
- # Parse parameters if available
81
- if 'parameters' in metadata:
82
- parsed = parse_parameters(metadata['parameters'])
83
- prompt, negative_prompt, adetailer_prompt_1, adetailer_prompt_2, seed_number = parsed
 
 
 
 
 
 
84
 
85
- # Direct extraction with validation for Comfy Prompt
86
- if 'prompt' in metadata and isinstance(metadata['prompt'], str):
87
- comfy_prompt = metadata['prompt'].strip()
88
- else:
89
- comfy_prompt = 'N/A' # Fallback if prompt is missing or not a string
 
 
 
 
 
 
 
 
90
 
91
- # Direct extraction with validation for Comfy Workflow
92
- if 'workflow' in metadata and isinstance(metadata['workflow'], str):
93
- comfy_workflow = metadata['workflow'].strip()
94
- else:
95
- comfy_workflow = 'N/A' # Fallback if workflow is missing or not a string
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  return (
98
- img_display,
99
- prompt,
100
- negative_prompt,
101
- seed_number,
102
- adetailer_prompt_1,
103
- adetailer_prompt_2,
104
- metadata_str,
105
- comfy_prompt,
106
- comfy_workflow
107
  )
108
 
 
109
  def export_workflow(workflow_text):
110
- """
111
- Converts the workflow text into JSON format and returns it as a downloadable file.
112
- """
113
- print("Export workflow function called.")
114
  if workflow_text == "N/A" or not workflow_text.strip():
115
- print("No workflow data to export.")
116
  return None, "No workflow data to export."
117
 
118
- # Structure the JSON data
119
- workflow_data = {
120
- "comfy_workflow": workflow_text
121
- }
122
 
123
- # Create a temporary JSON file
124
  try:
125
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json') as tmp_file:
 
 
126
  json.dump(workflow_data, tmp_file, indent=4)
127
  tmp_file_path = tmp_file.name
128
- print(f"Temporary JSON file created at: {tmp_file_path}")
129
- except Exception as e:
130
- print(f"Error creating temporary file: {e}")
131
  return None, "Failed to create workflow JSON file."
132
 
133
- # Check if the file was created successfully
134
  if os.path.exists(tmp_file_path):
135
- message = "Workflow exported successfully."
136
- print(message)
137
- return tmp_file_path, message
138
  else:
139
- message = "Failed to export workflow."
140
- print(message)
141
- return None, message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  def main():
144
- # Create Gradio User Interface
145
  with gr.Blocks() as iface:
146
- gr.Markdown("<h1>Automatic 1111/ Comfy Metadata Reader</h1>")
147
- gr.Markdown("<p>Upload an image (PNG, JPEG) to extract its metadata and parse it for prompts.</p>")
 
 
148
  with gr.Row():
149
- with gr.Column(): # First column for image upload and preview
150
- image_input = gr.File(label="Upload Image", type="filepath")
151
- image_output = gr.Image(label="Image Preview")
152
- original_metadata_output = gr.Textbox(label="Original Metadata", lines=20, interactive=False)
153
-
154
- with gr.Column(): # Second column for metadata outputs
155
- prompt_output = gr.Textbox(label="Prompt", lines=4, show_copy_button=True, interactive=False)
156
- negative_prompt_output = gr.Textbox(label="Negative Prompt", lines=4, show_copy_button=True, interactive=False)
157
- seed_output = gr.Textbox(label="Seed Number", lines=1, show_copy_button=True, interactive=False)
158
- adetailer_prompt_1_output = gr.Textbox(label="ADetailer Prompt 1", lines=3, show_copy_button=True, interactive=False)
159
- adetailer_prompt_2_output = gr.Textbox(label="ADetailer Prompt 2", lines=3, show_copy_button=True, interactive=False)
160
-
161
- with gr.Column(): # Third column for additional metadata outputs
162
- with gr.Row():
163
- comfy_prompt_output = gr.Textbox(label="Comfy Prompt", lines=4, value="N/A", interactive=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  with gr.Row():
165
- comfy_workflow_output = gr.Textbox(label="Comfy Workflow", lines=20, value="N/A", interactive=False)
166
  export_button = gr.Button("Export Workflow as JSON")
167
- workflow_file = gr.File(label="Download Workflow JSON", visible=False)
168
- with gr.Row():
169
- export_message = gr.Textbox(label="Export Status", lines=1, interactive=False, visible=False)
 
 
 
 
 
 
170
 
171
- # Set up the input-output relationships
172
  image_input.change(
173
  fn=extract_metadata,
174
  inputs=image_input,
175
  outputs=[
176
- image_output,
177
  prompt_output,
178
  negative_prompt_output,
179
  seed_output,
180
- adetailer_prompt_1_output,
181
- adetailer_prompt_2_output,
182
  original_metadata_output,
183
- comfy_prompt_output,
184
  comfy_workflow_output,
185
- ]
 
 
186
  )
187
 
188
- # Connect the export button to the export function
189
  export_button.click(
190
  fn=export_workflow,
191
  inputs=comfy_workflow_output,
192
  outputs=[workflow_file, export_message],
193
- queue=False
 
 
 
 
 
 
 
194
  )
195
 
196
  iface.launch()
197
 
 
198
  if __name__ == "__main__":
199
- main()
 
1
  import gradio as gr
2
  from PIL import Image
3
  from PIL.ExifTags import TAGS
 
4
  import re
5
  import json
6
  import tempfile
7
  import os
8
+ import math
9
+
10
+
11
+ def get_aspect_ratio(w, h):
12
+ """Return the closest standard aspect ratio string for given dimensions."""
13
+ if w == 0 or h == 0:
14
+ return "N/A"
15
+ ratio = w / h
16
+ tolerance = 0.02
17
+ ratios = {
18
+ "1:1": 1.0,
19
+ "5:4": 5 / 4,
20
+ "4:3": 4 / 3,
21
+ "3:2": 3 / 2,
22
+ "16:9": 16 / 9,
23
+ "16:10": 16 / 10,
24
+ "21:9": 21 / 9,
25
+ "2:3": 2 / 3,
26
+ "3:4": 3 / 4,
27
+ "4:5": 4 / 5,
28
+ "9:16": 9 / 16,
29
+ }
30
+ for label, val in ratios.items():
31
+ if abs(ratio - val) < tolerance:
32
+ return label
33
+ gcd = math.gcd(w, h)
34
+ return f"{w // gcd}:{h // gcd}"
35
+
36
 
37
  def get_image_metadata(img):
38
  """Extract metadata based on image format."""
39
  metadata = {}
40
+ if img.format == "PNG":
41
  metadata = img.info
42
+ elif img.format in ["JPEG", "TIFF", "WEBP"]:
43
  exif_data = img._getexif()
44
  if exif_data:
45
  for tag, value in exif_data.items():
 
47
  metadata[tag_name] = value
48
  return metadata
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
+ def parse_comfy_prompt(prompt_data):
52
+ """Parse ComfyUI prompt JSON using graph tracing for correct pos/neg identification."""
53
+ positive_prompt = "N/A"
54
+ negative_prompt = "N/A"
55
+ seed = "N/A"
56
 
57
+ try:
58
+ if isinstance(prompt_data, str):
59
+ prompt_data = json.loads(prompt_data)
60
+ except (json.JSONDecodeError, TypeError):
61
+ return positive_prompt, negative_prompt, seed, []
62
+
63
+ if not isinstance(prompt_data, dict):
64
+ return positive_prompt, negative_prompt, seed, []
65
+
66
+ clip_text_nodes = {}
67
+ lora_names = []
68
+
69
+ for node_id, node in prompt_data.items():
70
+ if not isinstance(node, dict):
71
+ continue
72
+ class_type = node.get("class_type", "")
73
+ inputs = node.get("inputs", {})
74
+ widgets = node.get("widgets_values", [])
75
 
76
+ if class_type in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):
77
+ text = inputs.get("text", "")
78
+ if not text or not text.strip():
79
+ continue
80
+ clip_text_nodes[node_id] = {
81
+ "text": text.strip(),
82
+ "links": node.get("inputs", []),
83
+ }
84
 
85
+ if "LoraLoader" in class_type or "LoraLoaderModelOnly" in class_type:
86
+ lora_name = inputs.get("lora_name", inputs.get("lora", ""))
87
+ if isinstance(lora_name, str) and lora_name.strip():
88
+ lora_names.append(lora_name.strip())
89
+ for wv in widgets:
90
+ if isinstance(wv, str) and wv.endswith(
91
+ (".safetensors", ".ckpt", ".pt")
92
+ ):
93
+ if wv not in lora_names:
94
+ lora_names.append(wv)
95
+
96
+ if class_type in (
97
+ "KSampler",
98
+ "KSamplerAdvanced",
99
+ "SamplerCustom",
100
+ "SamplerCustomAdvanced",
101
+ ):
102
+ if "seed" in inputs:
103
+ seed = str(inputs["seed"])
104
+ if "noise_seed" in inputs:
105
+ seed = str(inputs["noise_seed"])
106
+
107
+ positive_ids = set()
108
+ negative_ids = set()
109
+
110
+ for node_id, node in prompt_data.items():
111
+ if not isinstance(node, dict):
112
+ continue
113
+ class_type = node.get("class_type", "")
114
+ inputs = node.get("inputs", {})
115
+
116
+ is_sampler = class_type in (
117
+ "KSampler",
118
+ "KSamplerAdvanced",
119
+ "SamplerCustom",
120
+ "SamplerCustomAdvanced",
121
+ )
122
+ if is_sampler:
123
+ for input_name in ("positive", "latent_image"):
124
+ link = inputs.get(input_name)
125
+ if isinstance(link, list) and len(link) >= 1:
126
+ positive_ids.add(str(link[0]))
127
+ for input_name in ("negative",):
128
+ link = inputs.get(input_name)
129
+ if isinstance(link, list) and len(link) >= 1:
130
+ negative_ids.add(str(link[0]))
131
+
132
+ if class_type in ("SamplerCustom", "SamplerCustomAdvanced"):
133
+ for input_name in ("guider", "sampler", "sigmas"):
134
+ link = inputs.get(input_name)
135
+ if isinstance(link, list) and len(link) >= 1:
136
+ pass
137
+
138
+ def trace_positive(node_id, visited=None):
139
+ if visited is None:
140
+ visited = set()
141
+ if node_id in visited:
142
+ return []
143
+ visited.add(node_id)
144
+ node = prompt_data.get(node_id, {})
145
+ if not isinstance(node, dict):
146
+ return []
147
+ class_type = node.get("class_type", "")
148
+ inputs = node.get("inputs", {})
149
+ texts = []
150
+ if class_type in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):
151
+ text = inputs.get("text", "")
152
+ if text and text.strip():
153
+ texts.append(text.strip())
154
+ for input_name, link in inputs.items():
155
+ if isinstance(link, list) and len(link) >= 1:
156
+ texts.extend(trace_positive(str(link[0]), visited))
157
+ return texts
158
+
159
+ def trace_negative(node_id, visited=None):
160
+ if visited is None:
161
+ visited = set()
162
+ if node_id in visited:
163
+ return []
164
+ visited.add(node_id)
165
+ node = prompt_data.get(node_id, {})
166
+ if not isinstance(node, dict):
167
+ return []
168
+ class_type = node.get("class_type", "")
169
+ inputs = node.get("inputs", {})
170
+ texts = []
171
+ if class_type in ("CLIPTextEncode", "CLIPTextEncodeSDXL"):
172
+ text = inputs.get("text", "")
173
+ if text and text.strip():
174
+ texts.append(text.strip())
175
+ for input_name, link in inputs.items():
176
+ if isinstance(link, list) and len(link) >= 1:
177
+ texts.extend(trace_negative(str(link[0]), visited))
178
+ return texts
179
+
180
+ found_pos = False
181
+ found_neg = False
182
+ for nid in sorted(positive_ids):
183
+ if nid in clip_text_nodes:
184
+ positive_prompt = clip_text_nodes[nid]["text"]
185
+ found_pos = True
186
+ break
187
+ if not found_pos:
188
+ for nid in sorted(positive_ids):
189
+ texts = trace_positive(nid)
190
+ if texts:
191
+ positive_prompt = texts[0]
192
+ found_pos = True
193
+ break
194
+
195
+ for nid in sorted(negative_ids):
196
+ if nid in clip_text_nodes:
197
+ negative_prompt = clip_text_nodes[nid]["text"]
198
+ found_neg = True
199
+ break
200
+ if not found_neg:
201
+ for nid in sorted(negative_ids):
202
+ texts = trace_negative(nid)
203
+ if texts:
204
+ negative_prompt = texts[0]
205
+ found_neg = True
206
+ break
207
+
208
+ if not found_pos and not found_neg:
209
+ texts = [n["text"] for n in clip_text_nodes.values()]
210
+ if len(texts) >= 1:
211
+ positive_prompt = texts[0]
212
+ if len(texts) >= 2:
213
+ negative_prompt = texts[1]
214
+
215
+ return positive_prompt, negative_prompt, seed, lora_names
216
 
 
217
 
218
  def extract_metadata(image_file):
219
  """Extract and parse metadata from the uploaded image."""
220
  try:
221
  img = Image.open(image_file)
222
+ except Exception:
223
+ return ("Error: Unable to open image.", *(["N/A"] * 6), [], "N/A")
 
224
 
225
  metadata = get_image_metadata(img)
226
+ width, height = img.size
227
+ aspect_ratio = get_aspect_ratio(width, height)
228
+ dimensions = f"{width} × {height} ({aspect_ratio})"
229
 
 
230
  metadata_str = "\n".join([f"{key}: {value}" for key, value in metadata.items()])
231
 
232
+ prompt = "N/A"
233
+ negative_prompt = "N/A"
234
+ seed_number = "N/A"
235
+ comfy_workflow = "N/A"
236
+ lora_names = []
237
 
238
+ comfy_prompt_data = metadata.get("prompt", None)
239
+ if comfy_prompt_data is not None:
240
+ pos, neg, s, loras = parse_comfy_prompt(comfy_prompt_data)
241
+ if pos != "N/A":
242
+ prompt = pos
243
+ if neg != "N/A":
244
+ negative_prompt = neg
245
+ if s != "N/A":
246
+ seed_number = s
247
+ lora_names = loras
248
 
249
+ elif "parameters" in metadata:
250
+ params = metadata["parameters"]
251
+ prompt_match = re.search(
252
+ r"(.*?)Negative prompt:", params, re.DOTALL | re.IGNORECASE
253
+ )
254
+ if prompt_match:
255
+ prompt = prompt_match.group(1).strip()
256
+ else:
257
+ prompt_match = re.search(
258
+ r"(.*?)(Steps:|$)", params, re.DOTALL | re.IGNORECASE
259
+ )
260
+ if prompt_match:
261
+ prompt = prompt_match.group(1).strip()
262
 
263
+ neg_match = re.search(
264
+ r"Negative prompt:(.*?)(Steps:|$)", params, re.DOTALL | re.IGNORECASE
265
+ )
266
+ if neg_match:
267
+ negative_prompt = neg_match.group(1).strip()
268
+
269
+ seed_match = re.search(r"Seed:\s*(\d+)", params, re.IGNORECASE)
270
+ if seed_match:
271
+ seed_number = seed_match.group(1).strip()
272
+
273
+ lora_pattern = re.compile(r"<lora:([^:>]+)", re.IGNORECASE)
274
+ lora_names = lora_pattern.findall(params)
275
+
276
+ workflow_data = metadata.get("workflow", None)
277
+ if workflow_data is not None:
278
+ if isinstance(workflow_data, str):
279
+ try:
280
+ json.loads(workflow_data)
281
+ comfy_workflow = workflow_data
282
+ except json.JSONDecodeError:
283
+ comfy_workflow = workflow_data
284
+ elif isinstance(workflow_data, dict):
285
+ comfy_workflow = json.dumps(workflow_data, indent=2)
286
+ else:
287
+ comfy_workflow = str(workflow_data)
288
 
289
  return (
290
+ dimensions,
291
+ prompt,
292
+ negative_prompt,
293
+ seed_number,
294
+ metadata_str,
295
+ comfy_workflow,
296
+ lora_names,
297
+ "N/A",
 
298
  )
299
 
300
+
301
  def export_workflow(workflow_text):
302
+ """Convert the workflow text into a downloadable JSON file."""
 
 
 
303
  if workflow_text == "N/A" or not workflow_text.strip():
 
304
  return None, "No workflow data to export."
305
 
306
+ workflow_data = {"comfy_workflow": workflow_text}
 
 
 
307
 
 
308
  try:
309
+ with tempfile.NamedTemporaryFile(
310
+ mode="w", delete=False, suffix=".json"
311
+ ) as tmp_file:
312
  json.dump(workflow_data, tmp_file, indent=4)
313
  tmp_file_path = tmp_file.name
314
+ except Exception:
 
 
315
  return None, "Failed to create workflow JSON file."
316
 
 
317
  if os.path.exists(tmp_file_path):
318
+ return tmp_file_path, "Workflow exported successfully."
 
 
319
  else:
320
+ return None, "Failed to export workflow."
321
+
322
+
323
+ def strip_metadata(image_file):
324
+ """Strip all metadata from an image and return a clean file."""
325
+ if image_file is None:
326
+ return None, "No image provided."
327
+
328
+ try:
329
+ img = Image.open(image_file)
330
+ except Exception:
331
+ return None, "Error: Unable to open image."
332
+
333
+ clean_img = Image.new(img.mode, img.size)
334
+ clean_img.putdata(list(img.getdata()))
335
+
336
+ try:
337
+ with tempfile.NamedTemporaryFile(
338
+ mode="wb", delete=False, suffix=".png"
339
+ ) as tmp_file:
340
+ clean_img.save(tmp_file, format="PNG")
341
+ tmp_file_path = tmp_file.name
342
+ except Exception:
343
+ return None, "Error saving stripped image."
344
+
345
+ return tmp_file_path, "Metadata stripped successfully."
346
+
347
 
348
  def main():
 
349
  with gr.Blocks() as iface:
350
+ gr.Markdown("<h1>Comfy / A1111 Metadata Reader</h1>")
351
+ gr.Markdown(
352
+ "<p>Upload an image (PNG, JPEG, WebP) to extract its metadata and parse it for prompts.</p>"
353
+ )
354
  with gr.Row():
355
+ with gr.Column(scale=1):
356
+ image_input = gr.Image(label="Drop Image Here", type="filepath")
357
+
358
+ with gr.Column(scale=2):
359
+ dimensions_output = gr.Textbox(
360
+ label="Dimensions", lines=1, interactive=False
361
+ )
362
+ prompt_output = gr.Textbox(
363
+ label="Prompt", lines=4, buttons=["copy"], interactive=False
364
+ )
365
+ negative_prompt_output = gr.Textbox(
366
+ label="Negative Prompt",
367
+ lines=4,
368
+ buttons=["copy"],
369
+ interactive=False,
370
+ )
371
+ seed_output = gr.Textbox(
372
+ label="Seed Number", lines=1, buttons=["copy"], interactive=False
373
+ )
374
+ lora_output = gr.Textbox(
375
+ label="LoRA(s)", lines=2, buttons=["copy"], interactive=False
376
+ )
377
+ original_metadata_output = gr.Textbox(
378
+ label="Original Metadata", lines=15, interactive=False
379
+ )
380
+
381
+ with gr.Column(scale=2):
382
+ comfy_workflow_output = gr.Textbox(
383
+ label="Comfy Workflow", lines=20, value="N/A", interactive=False
384
+ )
385
  with gr.Row():
 
386
  export_button = gr.Button("Export Workflow as JSON")
387
+ strip_button = gr.Button("Strip Metadata", variant="stop")
388
+ workflow_file = gr.File(label="Download Workflow JSON", visible=False)
389
+ export_message = gr.Textbox(
390
+ label="Export Status", lines=1, interactive=False
391
+ )
392
+ strip_file = gr.File(label="Download Stripped Image", visible=False)
393
+ strip_message = gr.Textbox(
394
+ label="Strip Status", lines=1, interactive=False
395
+ )
396
 
 
397
  image_input.change(
398
  fn=extract_metadata,
399
  inputs=image_input,
400
  outputs=[
401
+ dimensions_output,
402
  prompt_output,
403
  negative_prompt_output,
404
  seed_output,
 
 
405
  original_metadata_output,
 
406
  comfy_workflow_output,
407
+ lora_output,
408
+ strip_message,
409
+ ],
410
  )
411
 
 
412
  export_button.click(
413
  fn=export_workflow,
414
  inputs=comfy_workflow_output,
415
  outputs=[workflow_file, export_message],
416
+ queue=False,
417
+ )
418
+
419
+ strip_button.click(
420
+ fn=strip_metadata,
421
+ inputs=image_input,
422
+ outputs=[strip_file, strip_message],
423
+ queue=False,
424
  )
425
 
426
  iface.launch()
427
 
428
+
429
  if __name__ == "__main__":
430
+ main()
requirements.txt CHANGED
@@ -1,2 +1,3 @@
1
  gradio
2
- Pillow
 
 
1
  gradio
2
+ Pillow
3
+ numpy