"""
ComfyUI Metadata Viewer
========================
Drag & drop a file produced by ComfyUI (PNG, MP3, FLAC, MP4...) and it
displays, as readable tables:
- the model and LoRAs used
- the main parameters (steps, cfg, sampler, seed, duration...)
- the final prompt actually used, and the "dynamic prompt" { | }
template that generated it, when there is one.
"""
import html
import re
import traceback
import gradio as gr
import spaces
from comfyui_meta import analyze, MetadataNotFound, StructuredView
BRACE_RE = re.compile(r"(\{[^{}]*\})")
@spaces.GPU(duration=1)
def _zerogpu_startup_probe():
"""This app never needs a GPU — it only reads metadata out of a
file. But Spaces running on the free ZeroGPU tier require at least
one @spaces.GPU-decorated function to exist, or the Space refuses
to start. This function exists only to satisfy that check; it is
never called."""
return None
def _highlight_template(text: str) -> str:
"""Escapes HTML and highlights {a | b | c} blocks."""
escaped = html.escape(text)
# re-apply the pattern on the escaped text (braces are unaffected
# by html.escape)
parts = BRACE_RE.split(escaped)
out = []
for part in parts:
if part.startswith("{") and part.endswith("}"):
out.append(f'{part}')
else:
out.append(part)
return "".join(out)
def _models_table(view: StructuredView):
rows = [[m.kind, m.name, m.detail, m.node] for m in view.models]
if not rows:
rows = [["—", "No model/LoRA detected", "", ""]]
return rows
def _params_table(view: StructuredView):
rows = [[p.node, p.param, p.value] for p in view.params]
if not rows:
rows = [["—", "No parameter detected", ""]]
return rows
def _prompts_html(view: StructuredView) -> str:
if not view.prompts:
return "
No prompt detected in this file.
"
cards = []
for p in view.prompts:
template_block = ""
if p.template:
template_block = f"""
🎲 Dynamic template (raw)
{_highlight_template(p.template)}
"""
cards.append(f"""
{html.escape(p.node)} — {html.escape(p.role)}
{template_block}
✅ Final prompt (resolved)
{html.escape(p.resolved)}
""")
style = """
"""
return style + "\n".join(cards)
def _warnings_html(view: StructuredView) -> str:
if not view.warnings:
return ""
items = "".join(f"{html.escape(w)}" for w in view.warnings)
return f""
def process_file(file_path):
if not file_path:
return [], [], "", "", "Drop a file above to get started."
try:
view = analyze(file_path)
except MetadataNotFound as e:
return [], [], "", "", f"❌ {e}"
except Exception:
return [], [], "", "", (
"❌ Unexpected error during analysis:\n\n" + traceback.format_exc()
)
status = "✅ ComfyUI metadata found and analyzed."
return (
_models_table(view),
_params_table(view),
_prompts_html(view),
_warnings_html(view),
status,
)
with gr.Blocks(title="ComfyUI Metadata Viewer") as demo:
gr.Markdown(
"# 🔍 ComfyUI Metadata Viewer\n"
"Drag & drop a file generated by ComfyUI (PNG, MP3, FLAC, MP4...) "
"to see the model, LoRAs, parameters and prompt (with its original "
"dynamic prompt, if there is one)."
)
file_input = gr.File(
label="ComfyUI file (drag & drop)",
file_types=[".png", ".webp", ".jpg", ".jpeg",
".mp3", ".flac", ".m4a", ".ogg",
".mp4", ".mov", ".webm", ".mkv"],
type="filepath",
)
clear_btn = gr.Button("🗑️ Clear", size="sm", variant="secondary")
status_box = gr.Markdown("Drop a file above to get started.")
with gr.Tab("🧩 Model & LoRAs"):
models_df = gr.Dataframe(
headers=["Type", "Name", "Detail", "Node"],
datatype=["str", "str", "str", "str"],
row_count=(0, "dynamic"),
wrap=True,
)
with gr.Tab("⚙️ Main Parameters"):
params_df = gr.Dataframe(
headers=["Node", "Parameter", "Value"],
datatype=["str", "str", "str"],
row_count=(0, "dynamic"),
wrap=True,
)
with gr.Tab("📝 Prompt & Dynamic Prompt"):
prompts_html = gr.HTML()
warnings_html = gr.HTML()
file_input.change(
fn=process_file,
inputs=[file_input],
outputs=[models_df, params_df, prompts_html, warnings_html, status_box],
)
def _clear_all():
return None, [], [], "", "", "Drop a file above to get started."
clear_btn.click(
fn=_clear_all,
inputs=[],
outputs=[file_input, models_df, params_df, prompts_html, warnings_html, status_box],
)
if __name__ == "__main__":
demo.launch()