akhaliq HF Staff commited on
Commit
89d839f
·
1 Parent(s): 4e2a3d4

Port the studio from gradio.Server + custom index.html to gr.Workflow

Browse files

- app.py: replace Server/@app.api/FastAPI routes with a gr.Workflow canvas
(workflow.json) bound to a generate_video fn operator; x-ip-token now
forwarded via LocalContext. All pipeline features unchanged: NCII guard,
larry/lightx/off LoRAs, keyframe cover-crop/canvas-fit, GPU duration
estimator, AoTI blocks.
- workflow.json: the canvas graph (references -> generate_video -> subjects).
- requirements.txt: Gradio from main via the prebuilt pypi-previews wheel at
a9ce60a (git installs ship no frontend assets); README sdk_version bumped
to 6.25.0 to match, hf_oauth: true for owner canvas editing.
- index.html removed: the Workflow canvas replaces the custom studio.

Files changed (5) hide show
  1. README.md +10 -8
  2. app.py +52 -56
  3. index.html +0 -478
  4. requirements.txt +4 -1
  5. workflow.json +562 -0
README.md CHANGED
@@ -4,9 +4,10 @@ emoji: 🎬
4
  colorFrom: purple
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
  app_file: app.py
9
  pinned: true
 
10
  short_description: Video generation with a synchronized soundtrack
11
  suggested_hardware: zero-a10g
12
  ---
@@ -35,14 +36,15 @@ Space is therefore impossible, which is why quantized demos of it run NVFP4 or f
35
  Besides the quality argument, unquantized weights are the ones AoTI can export; an NVFP4 checkpoint cannot be
36
  exported at all.
37
 
38
- ## Studio frontend (gradio.Server)
39
 
40
- The UI is a custom single-page studio (`index.html`) served by [`gradio.Server`](https://www.gradio.app/docs/gradio/server):
41
- `@app.get("/")` serves the page, `@app.api(name="generate")` keeps the request on Gradio's queue (concurrency control,
42
- SSE, ZeroGPU booking, `gradio_client` compatibility the API name and signature are unchanged), and the page talks
43
- to it with the `@gradio/client` JS package. `/status` and `/config` are plain FastAPI routes the page polls for
44
- readiness and the canvas table. Keyframe cover-crop / canvas fitting moved server-side into `generate`, so API
45
- callers get the same treatment the old upload event gave.
 
46
 
47
  ## 4-step Turbo LoRA
48
 
 
4
  colorFrom: purple
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 6.25.0
8
  app_file: app.py
9
  pinned: true
10
+ hf_oauth: true
11
  short_description: Video generation with a synchronized soundtrack
12
  suggested_hardware: zero-a10g
13
  ---
 
36
  Besides the quality argument, unquantized weights are the ones AoTI can export; an NVFP4 checkpoint cannot be
37
  exported at all.
38
 
39
+ ## Workflow frontend (gr.Workflow)
40
 
41
+ The UI is a [`gr.Workflow`](https://www.gradio.app/docs/gradio/workflow) canvas (`workflow.json`): reference nodes
42
+ for Prompt / First Frame / Last Frame / Canvas / Duration / Steps / Seed / Upsample / LoRA wire into a single
43
+ `generate_video` fn operator, and out to Output Video / Report / Refined Prompt subjects. The fn runs on Gradio's
44
+ queue (concurrency control, SSE, ZeroGPU booking, `gradio_client` compatibility), and the visitor's `x-ip-token`
45
+ is forwarded through `LocalContext` so the conditioner booking is billed to them. With `hf_oauth: true` the owner
46
+ can rewire the canvas in place; visitors get a runnable, read-only graph. Keyframe cover-crop / canvas fitting
47
+ runs server-side in `generate`, so every caller gets the same treatment.
48
 
49
  ## 4-step Turbo LoRA
50
 
app.py CHANGED
@@ -11,9 +11,7 @@ from functools import cache
11
  # Before anything that could initialize CUDA: `import spaces` patches `torch.cuda` so the 72 GiB load can happen at
12
  # startup rather than on GPU time.
13
  import spaces
14
- from fastapi.responses import HTMLResponse
15
- from gradio import Request, Server
16
- from gradio.data_classes import FileData
17
 
18
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
19
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
@@ -339,10 +337,14 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
339
 
340
  lora = _resolve_lora(lora, use_lora)
341
 
342
- # Server mode: keyframes arrive as FileData dicts, and the cover-crop / canvas-fit that used to be an upload
343
- # event in the Blocks UI runs here instead, so API callers get the same treatment.
344
- first = image_path["path"] if isinstance(image_path, dict) else image_path
345
- last = last_image_path["path"] if isinstance(last_image_path, dict) else last_image_path
 
 
 
 
346
  if first:
347
  first, canvas = _fit_keyframe(first, canvas)
348
  if last:
@@ -390,66 +392,60 @@ def generate(prompt, image_path=None, last_image_path=None, canvas=DEFAULT_CANVA
390
  f"turbo LoRA {active_lora} · seed {int(seed)}"
391
  )
392
  print(f"[gen] {report}", flush=True)
393
- return FileData(path=path), report, refined
 
394
 
395
 
396
 
397
  # ======================================================================
398
- # Server mode: Gradio's API engine (queue, SSE, concurrency, ZeroGPU,
399
- # gradio_client) under a fully custom studio frontend (index.html).
 
400
  # ======================================================================
401
- app = Server(title="MiniMax-H3 Studio")
402
-
403
-
404
- @app.api(name="generate")
405
- def _generate_api(prompt: str, image_path: FileData | None = None, last_image_path: FileData | None = None,
406
- canvas: str = DEFAULT_CANVAS, duration: float = 5, steps: int = 6, seed: float = 42,
407
- upsample: bool = False, use_lora: bool = True, lora: str = "", request: Request = None) -> tuple[FileData, str, str]:
408
- """Generate a video with a synchronized soundtrack. Returns (video, report, refined prompt).
409
 
410
- `lora` selects the turbo LoRA: `larry` (default), `lightx`, or `off`. The legacy `use_lora` bool still works
411
- when `lora` is empty.
412
  """
413
- # `request` is injected by the event system, not an API input; its x-ip-token bills the conditioner to the caller.
414
- ip_token = request.headers.get("x-ip-token") if request is not None else None
415
- return generate(prompt, image_path, last_image_path, canvas, duration, steps, seed, upsample, use_lora, lora, ip_token=ip_token)
416
 
 
 
417
 
418
- @app.get("/status")
419
- def studio_status():
420
- """Polled by the frontend: is the denoiser ready, and the human-readable status line."""
421
- return {"ready": PIPE is not None and LOAD_ERROR is None, "status": status()}
422
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
- # NB: not `/config` — Gradio's own client-discovery route lives there and shadowing it breaks `@gradio/client`.
425
- @app.get("/studio-config")
426
- def studio_config():
427
- """The canvas table and slider ranges, so the frontend never hardcodes a label the backend would reject."""
428
- import h3_lora
429
 
430
- state = getattr(PIPE.transformer, "_lora_state", None) if PIPE is not None else None
431
- sets = state["sets"] if state else {}
432
- return {
433
- "canvases": list(CANVASES),
434
- "default_canvas": DEFAULT_CANVAS,
435
- "min_duration": MIN_UI_DURATION,
436
- "max_duration": MAX_UI_DURATION,
437
- # The LoRA dropdown: value -> {label, suggested steps}.
438
- "loras": {
439
- **{
440
- name: {"label": spec["label"], "steps": {"larry": 6, "lightx": 4}.get(name, 6)}
441
- for name, spec in sets.items()
442
- },
443
- "off": {"label": "off (base model)", "steps": 28},
444
- },
445
- "default_lora": state["active"] if state else "off",
446
- }
447
-
448
-
449
- @app.get("/", response_class=HTMLResponse)
450
- def homepage():
451
- with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html"), encoding="utf-8") as f:
452
- return f.read()
453
 
454
 
455
  import ncii_guard
@@ -459,4 +455,4 @@ load_models()
459
 
460
  if __name__ == "__main__":
461
  # allowed_paths: the /gradio_api/file= route only serves whitelisted directories.
462
- app.launch(show_error=True, allowed_paths=[OUTPUT_DIR])
 
11
  # Before anything that could initialize CUDA: `import spaces` patches `torch.cuda` so the 72 GiB load can happen at
12
  # startup rather than on GPU time.
13
  import spaces
14
+ import gradio as gr
 
 
15
 
16
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
17
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
 
337
 
338
  lora = _resolve_lora(lora, use_lora)
339
 
340
+ # Keyframes arrive as FileData-style dicts (workflow canvas), plain paths, or URLs; the cover-crop /
341
+ # canvas-fit runs here so every caller gets the same treatment.
342
+ def _as_path(value):
343
+ if isinstance(value, dict):
344
+ value = value.get("path") or (value.get("url") or "").removeprefix("/gradio_api/file=")
345
+ return value or None
346
+
347
+ first, last = _as_path(image_path), _as_path(last_image_path)
348
  if first:
349
  first, canvas = _fit_keyframe(first, canvas)
350
  if last:
 
392
  f"turbo LoRA {active_lora} · seed {int(seed)}"
393
  )
394
  print(f"[gen] {report}", flush=True)
395
+ video = {"path": path, "url": f"/gradio_api/file={path}", "orig_name": os.path.basename(path), "mime_type": "video/mp4"}
396
+ return video, report, refined
397
 
398
 
399
 
400
  # ======================================================================
401
+ # Workflow mode: a gr.Workflow canvas (workflow.json) whose single fn
402
+ # operator calls `generate_video` below the same pipeline the old
403
+ # custom studio drove, now as a node visitors can rewire.
404
  # ======================================================================
405
+ def _caller_ip_token() -> str | None:
406
+ """The visitor's x-ip-token, so the conditioner booking is billed to them rather than this Space's pod IP.
 
 
 
 
 
 
407
 
408
+ Workflow fn nodes run inside Gradio's request context, so `LocalContext` carries the incoming request; when it
409
+ does not (API calls without the header), the shared client falls back to the pod's quota.
410
  """
411
+ from gradio.context import LocalContext
 
 
412
 
413
+ request = LocalContext.request.get()
414
+ return request.headers.get("x-ip-token") if request is not None else None
415
 
 
 
 
 
416
 
417
+ def generate_video(prompt: str, first_frame=None, last_frame=None, canvas: str = DEFAULT_CANVAS,
418
+ duration: float = 5, steps: float = 6, seed: float = 42, upsample: bool = False,
419
+ lora: str = "larry"):
420
+ """The workflow's `generate_video` fn operator. Returns (video file dict, report, refined prompt).
421
+
422
+ `lora` selects the turbo LoRA: `larry` (default), `lightx`, or `off`.
423
+ """
424
+ if LOAD_ERROR:
425
+ raise gr.Error(LOAD_ERROR.replace("**", "").replace("`", ""))
426
+ if PIPE is None:
427
+ raise gr.Error("The denoiser is still loading — watch the Space logs and retry shortly.")
428
+ if not prompt or not str(prompt).strip():
429
+ raise gr.Error("MiniMax-H3 always takes a prompt, keyframes or not.")
430
+ try:
431
+ return generate(str(prompt), first_frame, last_frame, canvas, float(duration), int(steps), float(seed),
432
+ bool(upsample), lora=str(lora or "larry"), ip_token=_caller_ip_token())
433
+ except gr.Error:
434
+ raise
435
+ except Exception as error:
436
+ message = str(error).lower()
437
+ if any(hint in message for hint in ("gpu limit", "quota", "could not allocate", "too many", "concurrent")):
438
+ raise gr.Error(
439
+ "The shared ZeroGPU pool is at capacity right now — not a problem with your inputs or account. "
440
+ "Wait a minute and retry."
441
+ ) from error
442
+ raise
443
 
 
 
 
 
 
444
 
445
+ # The graph wires Prompt / First Frame / Last Frame / Canvas / Duration / Steps / Seed / Upsample / LoRA into the
446
+ # operator and out to Output Video / Report / Refined Prompt subjects. Editable by the owner on the canvas when the
447
+ # Space sets `hf_oauth: true`; visitors get a runnable, read-only canvas.
448
+ demo = gr.Workflow(graph="workflow.json", bind={"generate_video": generate_video})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
 
451
  import ncii_guard
 
455
 
456
  if __name__ == "__main__":
457
  # allowed_paths: the /gradio_api/file= route only serves whitelisted directories.
458
+ demo.launch(show_error=True, allowed_paths=[OUTPUT_DIR])
index.html DELETED
@@ -1,478 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>MiniMax-H3 Studio</title>
7
- <style>
8
- :root {
9
- --bg: #0b0d10;
10
- --panel: #12151a;
11
- --panel-2: #171b21;
12
- --edge: #23282f;
13
- --edge-hi: #31383f;
14
- --text: #e8eaed;
15
- --dim: #9aa3ad;
16
- --faint: #5c6670;
17
- --accent: #f59e0b;
18
- --accent-dim: #92610a;
19
- --go: #22c55e;
20
- --err: #ef4444;
21
- --mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
22
- }
23
- * { margin: 0; box-sizing: border-box; }
24
- body {
25
- background: var(--bg); color: var(--text);
26
- font: 14px/1.5 -apple-system, "Segoe UI", Inter, Roboto, sans-serif;
27
- height: 100vh; display: flex; flex-direction: column; overflow: hidden;
28
- }
29
-
30
- /* ---- top bar ---- */
31
- header {
32
- display: flex; align-items: center; gap: 14px;
33
- padding: 0 18px; height: 52px; flex: none;
34
- background: var(--panel); border-bottom: 1px solid var(--edge);
35
- }
36
- .logo { font-weight: 700; letter-spacing: .4px; font-size: 15px; }
37
- .logo b { color: var(--accent); }
38
- .logo span { color: var(--faint); font-weight: 400; margin-left: 8px; font-size: 12px; }
39
- header .links { margin-left: auto; display: flex; gap: 14px; align-items: center; }
40
- header a { color: var(--dim); text-decoration: none; font-size: 12px; }
41
- header a:hover { color: var(--text); }
42
- #status-pill {
43
- display: flex; align-items: center; gap: 7px;
44
- font: 11px var(--mono); color: var(--dim);
45
- border: 1px solid var(--edge); border-radius: 99px; padding: 4px 12px;
46
- max-width: 46vw; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
47
- }
48
- #status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; animation: pulse 1.2s infinite; }
49
- #status-pill.ready #status-dot { background: var(--go); animation: none; }
50
- #status-pill.error #status-dot { background: var(--err); animation: none; }
51
- @keyframes pulse { 50% { opacity: .35; } }
52
-
53
- main { flex: 1; display: flex; min-height: 0; }
54
-
55
- /* ---- control deck ---- */
56
- aside {
57
- width: 340px; flex: none; overflow-y: auto;
58
- background: var(--panel); border-right: 1px solid var(--edge);
59
- padding: 16px; display: flex; flex-direction: column; gap: 14px;
60
- }
61
- .deck-label {
62
- font: 10px var(--mono); letter-spacing: 1.5px; color: var(--faint);
63
- text-transform: uppercase; margin-bottom: 6px;
64
- }
65
- textarea, select, input[type=number] {
66
- width: 100%; background: var(--panel-2); color: var(--text);
67
- border: 1px solid var(--edge); border-radius: 8px;
68
- padding: 10px 12px; font: 13px/1.5 inherit; resize: vertical;
69
- }
70
- textarea:focus, select:focus, input:focus { outline: none; border-color: var(--accent-dim); }
71
- textarea { min-height: 96px; }
72
-
73
- .dropzone {
74
- border: 1.5px dashed var(--edge-hi); border-radius: 8px;
75
- min-height: 74px; display: flex; align-items: center; justify-content: center;
76
- color: var(--faint); font-size: 12px; cursor: pointer; position: relative;
77
- overflow: hidden; text-align: center; padding: 6px; transition: border-color .15s;
78
- }
79
- .dropzone:hover, .dropzone.drag { border-color: var(--accent); color: var(--dim); }
80
- .dropzone img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
81
- .dropzone .clear {
82
- position: absolute; top: 4px; right: 6px; z-index: 2; color: #fff;
83
- background: rgba(0,0,0,.6); border-radius: 4px; padding: 0 6px; font-size: 14px; display: none;
84
- }
85
- .dropzone.filled .clear { display: block; }
86
- .frames-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
87
-
88
- .check { display: flex; align-items: center; gap: 8px; color: var(--dim); font-size: 13px; cursor: pointer; }
89
- .check input { accent-color: var(--accent); }
90
-
91
- details { border: 1px solid var(--edge); border-radius: 8px; background: var(--panel-2); }
92
- summary {
93
- padding: 9px 12px; cursor: pointer; font: 11px var(--mono);
94
- letter-spacing: 1px; color: var(--dim); text-transform: uppercase; user-select: none;
95
- }
96
- details .body { padding: 4px 12px 12px; display: flex; flex-direction: column; gap: 12px; }
97
- .slider-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--dim); margin-bottom: 2px; }
98
- .slider-row output { font-family: var(--mono); color: var(--text); }
99
- input[type=range] { width: 100%; accent-color: var(--accent); }
100
-
101
- #run {
102
- margin-top: auto; border: none; border-radius: 8px; padding: 13px;
103
- background: var(--accent); color: #111; font: 700 14px inherit;
104
- letter-spacing: .5px; cursor: pointer; transition: filter .15s, opacity .15s;
105
- }
106
- #run:hover:not(:disabled) { filter: brightness(1.1); }
107
- #run:disabled { opacity: .45; cursor: not-allowed; }
108
-
109
- /* ---- stage ---- */
110
- section.stage { flex: 1; display: flex; flex-direction: column; min-width: 0; }
111
- .monitor-wrap {
112
- flex: 1; display: flex; align-items: center; justify-content: center;
113
- padding: 22px; min-height: 0;
114
- background: radial-gradient(ellipse at 50% 40%, #10141a 0%, var(--bg) 75%);
115
- }
116
- .monitor {
117
- position: relative; max-width: 100%; max-height: 100%;
118
- border: 1px solid var(--edge-hi); border-radius: 10px; overflow: hidden;
119
- background: #000; box-shadow: 0 24px 70px rgba(0,0,0,.55);
120
- display: flex; align-items: center; justify-content: center;
121
- }
122
- .monitor video { display: block; max-width: 100%; max-height: calc(100vh - 220px); }
123
- .monitor .placeholder {
124
- position: absolute; inset: 0; display: flex; flex-direction: column;
125
- align-items: center; justify-content: center; gap: 10px; color: var(--faint);
126
- font: 12px var(--mono); letter-spacing: 1px; text-align: center; padding: 20px;
127
- }
128
- .monitor .placeholder .rec { width: 46px; height: 46px; border: 1.5px solid var(--edge-hi); border-radius: 50%;
129
- display: flex; align-items: center; justify-content: center; }
130
- .monitor .placeholder .rec::after { content: ""; width: 14px; height: 14px; border-radius: 50%; background: var(--edge-hi); }
131
- .monitor.hidden-video video { display: none; }
132
-
133
- /* ---- transport / report bar ---- */
134
- .transport {
135
- flex: none; border-top: 1px solid var(--edge); background: var(--panel);
136
- padding: 10px 18px; display: flex; align-items: center; gap: 16px;
137
- font: 12px var(--mono); color: var(--dim); min-height: 44px;
138
- }
139
- #job-state { color: var(--accent); }
140
- #job-state.done { color: var(--go); }
141
- #job-state.failed { color: var(--err); }
142
- #report { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
143
- #elapsed { color: var(--faint); flex: none; }
144
-
145
- #bar-track {
146
- flex: none; width: 180px; height: 6px; border-radius: 3px;
147
- background: var(--edge); overflow: hidden; position: relative;
148
- }
149
- #bar { height: 100%; width: 0%; border-radius: 3px; background: var(--accent); transition: width .4s linear; }
150
- #bar.indeterminate { width: 40%; animation: slide 1.2s ease-in-out infinite; }
151
- @keyframes slide { 0% { margin-left: -40%; } 100% { margin-left: 100%; } }
152
-
153
- #refined-bar {
154
- flex: none; display: none; border-top: 1px solid var(--edge);
155
- background: var(--panel-2); padding: 10px 18px; font-size: 12px; color: var(--dim);
156
- max-height: 110px; overflow-y: auto;
157
- }
158
- #refined-bar b { color: var(--faint); font: 10px var(--mono); letter-spacing: 1px; text-transform: uppercase; display: block; margin-bottom: 4px; }
159
-
160
- .examples { display: flex; flex-direction: column; gap: 6px; }
161
- .examples button {
162
- text-align: left; background: var(--panel-2); border: 1px solid var(--edge);
163
- color: var(--dim); border-radius: 6px; padding: 7px 10px; font-size: 12px; cursor: pointer;
164
- }
165
- .examples button:hover { color: var(--text); border-color: var(--edge-hi); }
166
-
167
- @media (max-width: 860px) {
168
- body { height: 100dvh; overflow: hidden; }
169
- main { flex-direction: column; overflow-y: auto; -webkit-overflow-scrolling: touch; }
170
-
171
- /* Stage first, controls below: the monitor is what a phone user came for. */
172
- section.stage { order: -1; flex: none; }
173
- .monitor-wrap { padding: 12px; }
174
- .monitor video { max-height: 38vh; }
175
- .monitor .placeholder { position: static; padding: 42px 16px; }
176
-
177
- aside {
178
- width: 100%; flex: none; overflow: visible;
179
- border-right: none; border-top: 1px solid var(--edge);
180
- padding: 14px 14px 90px; /* room for the sticky run button */
181
- }
182
- #run {
183
- position: sticky; bottom: 12px; z-index: 5;
184
- padding: 15px; font-size: 15px;
185
- box-shadow: 0 8px 24px rgba(0,0,0,.5);
186
- }
187
-
188
- /* Touch targets + kill iOS auto-zoom on focus (needs >=16px). */
189
- textarea, select, input[type=number] { font-size: 16px; }
190
- .dropzone { min-height: 92px; }
191
- .check { padding: 6px 0; }
192
- summary { padding: 12px; }
193
- input[type=range] { height: 28px; }
194
-
195
- header { padding: 0 12px; gap: 10px; }
196
- .logo span { display: none; }
197
- #status-pill { max-width: 38vw; padding: 4px 9px; }
198
- header .links a { display: none; }
199
- header .links #status-pill { display: flex; }
200
-
201
- .transport { flex-wrap: wrap; gap: 8px 12px; padding: 10px 12px; }
202
- #bar-track { flex: 1 1 100%; order: -1; width: auto; height: 5px; }
203
- #report { white-space: normal; flex: 1 1 60%; font-size: 11px; }
204
- #refined-bar { max-height: 80px; }
205
- }
206
- @media (max-width: 400px) {
207
- .frames-row { grid-template-columns: 1fr; }
208
- }
209
- </style>
210
- </head>
211
- <body>
212
-
213
- <header>
214
- <div class="logo">MiniMax-<b>H3</b> Studio<span>video + synchronized soundtrack · 4-step turbo</span></div>
215
- <div class="links">
216
- <div id="status-pill"><span id="status-dot"></span><span id="status-text">connecting…</span></div>
217
- <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3" target="_blank" rel="noopener">model</a>
218
- <a href="https://huggingface.co/larryvrh/MiniMax-H3-Turbo-Lora" target="_blank" rel="noopener">turbo lora</a>
219
- </div>
220
- </header>
221
-
222
- <main>
223
- <aside>
224
- <div>
225
- <div class="deck-label">Prompt</div>
226
- <textarea id="prompt" spellcheck="false">A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot</textarea>
227
- </div>
228
- <div>
229
- <div class="deck-label">Turbo LoRA</div>
230
- <select id="lora"></select>
231
- </div>
232
- <label class="check"><input type="checkbox" id="upsample"> Upsample prompt</label>
233
-
234
- <div>
235
- <div class="deck-label">Keyframes (optional)</div>
236
- <div class="frames-row">
237
- <div class="dropzone" id="dz-first">First frame<span class="clear">×</span></div>
238
- <div class="dropzone" id="dz-last">Last frame<span class="clear">×</span></div>
239
- </div>
240
- </div>
241
-
242
- <details open>
243
- <summary>Shot settings</summary>
244
- <div class="body">
245
- <div>
246
- <div class="deck-label">Canvas</div>
247
- <select id="canvas"></select>
248
- </div>
249
- <div>
250
- <div class="slider-row"><span>Duration</span><output id="duration-out">5 s</output></div>
251
- <input type="range" id="duration" min="2" max="14" step="1" value="5">
252
- </div>
253
- <div>
254
- <div class="slider-row"><span>Steps</span><output id="steps-out">6</output></div>
255
- <input type="range" id="steps" min="2" max="40" step="1" value="6">
256
- </div>
257
- <div>
258
- <div class="deck-label">Seed</div>
259
- <input type="number" id="seed" value="42" step="1">
260
- </div>
261
- </div>
262
- </details>
263
-
264
- <div>
265
- <div class="deck-label">Examples</div>
266
- <div class="examples" id="examples"></div>
267
- </div>
268
-
269
- <button id="run">▶&nbsp; Generate</button>
270
- </aside>
271
-
272
- <section class="stage">
273
- <div class="monitor-wrap">
274
- <div class="monitor hidden-video" id="monitor">
275
- <video id="video" controls playsinline></video>
276
- <div class="placeholder" id="placeholder">
277
- <div class="rec"></div>
278
- <div>STANDBY — CUT A PROMPT AND ROLL</div>
279
- </div>
280
- </div>
281
- </div>
282
- <div id="refined-bar"><b>Upsampled prompt</b><span id="refined"></span></div>
283
- <div class="transport">
284
- <span id="job-state">IDLE</span>
285
- <div id="bar-track"><div id="bar"></div></div>
286
- <span id="report"></span>
287
- <span id="elapsed"></span>
288
- </div>
289
- </section>
290
- </main>
291
-
292
- <input type="file" id="file-first" accept="image/*" hidden>
293
- <input type="file" id="file-last" accept="image/*" hidden>
294
-
295
- <script type="module">
296
- // @gradio/client is REQUIRED here (not plain fetch): it forwards the HF iframe auth headers that ZeroGPU
297
- // quota handling depends on — without them every booking lands on the pod IP's shared quota.
298
- import { Client, handle_file } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
299
-
300
- const $ = (id) => document.getElementById(id);
301
- const state = { first: null, last: null, busy: false, timer: null };
302
-
303
- /* ---- config + status ---- */
304
- fetch("/studio-config").then(r => r.json()).then(cfg => {
305
- const sel = $("canvas");
306
- for (const label of cfg.canvases) {
307
- const o = document.createElement("option");
308
- o.value = o.textContent = label;
309
- if (label === cfg.default_canvas) o.selected = true;
310
- sel.appendChild(o);
311
- }
312
- $("duration").min = cfg.min_duration; $("duration").max = cfg.max_duration;
313
-
314
- const loraSel = $("lora");
315
- for (const [value, spec] of Object.entries(cfg.loras || { larry: { label: "larry", steps: 6 } })) {
316
- const o = document.createElement("option");
317
- o.value = value;
318
- o.textContent = value === "off" ? "off (base model)" : value;
319
- o.title = spec.label;
320
- if (value === cfg.default_lora) o.selected = true;
321
- loraSel.appendChild(o);
322
- }
323
- // Each LoRA has a design point: suggest it on switch (the slider stays free).
324
- loraSel.addEventListener("change", () => {
325
- const spec = (cfg.loras || {})[loraSel.value];
326
- if (spec && spec.steps) { $("steps").value = spec.steps; $("steps-out").textContent = spec.steps; }
327
- });
328
- });
329
-
330
- async function pollStatus() {
331
- try {
332
- const s = await (await fetch("/status")).json();
333
- const pill = $("status-pill");
334
- $("status-text").textContent = s.status.replace(/[*`]/g, "");
335
- pill.classList.toggle("ready", s.ready);
336
- pill.classList.toggle("error", !s.ready && /failed/i.test(s.status));
337
- if (s.ready) return;
338
- } catch (e) { /* still booting */ }
339
- setTimeout(pollStatus, 5000);
340
- }
341
- pollStatus();
342
-
343
- /* ---- sliders ---- */
344
- const bind = (id, fmt) => $(id).addEventListener("input", e => $(id + "-out").textContent = fmt(e.target.value));
345
- bind("duration", v => v + " s");
346
- bind("steps", v => v);
347
-
348
- /* ---- dropzones ---- */
349
- function wireDropzone(dzId, inputId, key) {
350
- const dz = $(dzId), input = $(inputId);
351
- const set = (file) => {
352
- if (!file) return;
353
- state[key] = file;
354
- const img = document.createElement("img");
355
- img.src = URL.createObjectURL(file);
356
- dz.appendChild(img);
357
- dz.classList.add("filled");
358
- };
359
- dz.addEventListener("click", (e) => {
360
- if (e.target.classList.contains("clear")) {
361
- state[key] = null; dz.classList.remove("filled");
362
- dz.querySelector("img")?.remove(); input.value = "";
363
- } else input.click();
364
- });
365
- input.addEventListener("change", () => set(input.files[0]));
366
- dz.addEventListener("dragover", e => { e.preventDefault(); dz.classList.add("drag"); });
367
- dz.addEventListener("dragleave", () => dz.classList.remove("drag"));
368
- dz.addEventListener("drop", e => { e.preventDefault(); dz.classList.remove("drag"); set(e.dataTransfer.files[0]); });
369
- }
370
- wireDropzone("dz-first", "file-first", "first");
371
- wireDropzone("dz-last", "file-last", "last");
372
-
373
- /* ---- examples ---- */
374
- const EXAMPLES = [
375
- ["A red fox trotting through a snowy pine forest at dawn, snow crunching underfoot", "1344x768 · 16:9 full"],
376
- ["A busy night market, neon signs reflecting in puddles, sizzling street food", "768x1344 · 9:16 full"],
377
- ["A cellist playing a slow melody in an empty concert hall", "768x768 · 1:1 full"],
378
- ["Waves crashing against basalt cliffs at golden hour, gulls crying overhead", "1152x640 · 16:9"],
379
- ];
380
- for (const [p, c] of EXAMPLES) {
381
- const b = document.createElement("button");
382
- b.textContent = p.length > 60 ? p.slice(0, 60) + "…" : p;
383
- b.title = p;
384
- b.onclick = () => { $("prompt").value = p; $("canvas").value = c; };
385
- $("examples").appendChild(b);
386
- }
387
-
388
- /* ---- generate ---- */
389
- const client = await Client.connect(window.location.origin, { events: ["data", "status"] });
390
-
391
- function setJob(label, cls) {
392
- const el = $("job-state");
393
- el.textContent = label; el.className = cls || "";
394
- }
395
-
396
- function setBar(pct) {
397
- const bar = $("bar");
398
- if (pct === null) { bar.className = "indeterminate"; return; }
399
- bar.className = "";
400
- bar.style.width = Math.max(0, Math.min(100, pct)) + "%";
401
- }
402
-
403
- $("run").addEventListener("click", async () => {
404
- if (state.busy) return;
405
- const prompt = $("prompt").value.trim();
406
- if (!prompt) { setJob("FAILED", "failed"); $("report").textContent = "a prompt is required"; return; }
407
-
408
- state.busy = true;
409
- $("run").disabled = true;
410
- $("refined-bar").style.display = "none";
411
- $("report").textContent = "";
412
- setJob("ROLLING", "");
413
- const t0 = Date.now();
414
- state.timer = setInterval(() => $("elapsed").textContent = ((Date.now() - t0) / 1000).toFixed(0) + "s", 500);
415
-
416
- try {
417
- const submission = client.submit("/generate", {
418
- prompt,
419
- image_path: state.first ? handle_file(state.first) : null,
420
- last_image_path: state.last ? handle_file(state.last) : null,
421
- canvas: $("canvas").value,
422
- duration: Number($("duration").value),
423
- steps: Number($("steps").value),
424
- seed: Number($("seed").value),
425
- upsample: $("upsample").checked,
426
- lora: $("lora").value,
427
- });
428
- let data = null;
429
- for await (const msg of submission) {
430
- if (msg.type === "status") {
431
- if (msg.stage === "pending") {
432
- setJob(msg.position != null ? `QUEUED #${msg.position + 1}` : "QUEUED");
433
- setBar(null);
434
- } else if (msg.stage === "generating") {
435
- setJob("ROLLING");
436
- // ETA comes from the queue's booking estimate; blend it with our own elapsed timer.
437
- if (msg.eta != null && msg.eta > 0) {
438
- const elapsed = (Date.now() - t0) / 1000;
439
- setBar(100 * elapsed / (elapsed + msg.eta));
440
- } else setBar(null);
441
- } else if (msg.stage === "error") {
442
- throw new Error(msg.message || "generation failed");
443
- }
444
- } else if (msg.type === "data") {
445
- data = msg.data;
446
- }
447
- }
448
- if (!data) throw new Error("the backend returned no data");
449
- // Server mode returns the tuple as ONE Api output: unwrap [[video, report, refined]] too.
450
- if (data.length === 1 && Array.isArray(data[0])) data = data[0];
451
- const [video, report, refined] = data;
452
- // FileData may carry only `path`; resolve those through Gradio's file route.
453
- const videoUrl = (video && (video.url || (video.path && `/gradio_api/file=${video.path}`)))
454
- || (typeof video === "string" && `/gradio_api/file=${video}`);
455
- if (!videoUrl) throw new Error("the backend returned no video reference");
456
- $("video").src = videoUrl;
457
- $("monitor").classList.remove("hidden-video");
458
- $("placeholder").style.display = "none";
459
- $("report").textContent = report;
460
- if (refined) {
461
- $("refined").textContent = refined;
462
- $("refined-bar").style.display = "block";
463
- }
464
- setJob("DONE", "done");
465
- setBar(100);
466
- } catch (e) {
467
- setJob("FAILED", "failed");
468
- setBar(0);
469
- $("report").textContent = (e && e.message) ? e.message.slice(0, 300) : String(e);
470
- } finally {
471
- clearInterval(state.timer);
472
- state.busy = false;
473
- $("run").disabled = false;
474
- }
475
- });
476
- </script>
477
- </body>
478
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -14,7 +14,10 @@ transformers==5.8.0
14
  accelerate==1.14.0
15
  # diffusers pins <2.
16
  huggingface-hub==1.24.0
17
- gradio==6.20.0
 
 
 
18
  spaces==0.51.1
19
  # No `kernels` pin on purpose: the Hub attention backends want `kernels>=0.12.3`, and that version breaks
20
  # transformers 5.8.0 at import.
 
14
  accelerate==1.14.0
15
  # diffusers pins <2.
16
  huggingface-hub==1.24.0
17
+ # Gradio tracks main (https://github.com/gradio-app/gradio) via the prebuilt preview wheel — installing from git
18
+ # does not build the frontend assets. a9ce60ae03123ed6374b739d862e62dcae2f451e = refs/heads/main at the time of
19
+ # this PR; re-pin to the new head (and bump `sdk_version` in README.md if the version changed) to move past it.
20
+ gradio @ https://huggingface.co/buckets/gradio/pypi-previews/resolve/a9ce60ae03123ed6374b739d862e62dcae2f451e/gradio-6.25.0-py3-none-any.whl
21
  spaces==0.51.1
22
  # No `kernels` pin on purpose: the Hub attention backends want `kernels>=0.12.3`, and that version breaks
23
  # transformers 5.8.0 at import.
workflow.json ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "2",
3
+ "name": "MiniMax-H3 Studio",
4
+ "description": "Joint video + synchronized soundtrack from MiniMax-H3 (unquantized bf16, split deployment) with switchable turbo LoRAs, driven by a gr.Workflow fn-bound @spaces.GPU function.",
5
+ "runtime": {
6
+ "default": "client"
7
+ },
8
+ "view": {
9
+ "default": "canvas"
10
+ },
11
+ "references": [
12
+ {
13
+ "id": "ref_prompt",
14
+ "label": "Prompt",
15
+ "role": "reference",
16
+ "asset_type": "text",
17
+ "inputs": [
18
+ {
19
+ "id": "in",
20
+ "label": "Prompt",
21
+ "type": "text"
22
+ }
23
+ ],
24
+ "outputs": [
25
+ {
26
+ "id": "out",
27
+ "label": "Prompt",
28
+ "type": "text"
29
+ }
30
+ ],
31
+ "x": 40,
32
+ "y": 60,
33
+ "width": 280,
34
+ "height": 140,
35
+ "data": {
36
+ "out": "A corgi in a chef hat flipping a pancake in a sunlit kitchen, sizzling sounds and a cheerful bark, cinematic"
37
+ }
38
+ },
39
+ {
40
+ "id": "ref_first",
41
+ "label": "First Frame (optional)",
42
+ "role": "reference",
43
+ "asset_type": "image",
44
+ "inputs": [
45
+ {
46
+ "id": "in",
47
+ "label": "Image",
48
+ "type": "image"
49
+ }
50
+ ],
51
+ "outputs": [
52
+ {
53
+ "id": "out",
54
+ "label": "Image",
55
+ "type": "image"
56
+ }
57
+ ],
58
+ "x": 40,
59
+ "y": 230,
60
+ "width": 220,
61
+ "height": 120,
62
+ "data": {}
63
+ },
64
+ {
65
+ "id": "ref_last",
66
+ "label": "Last Frame (optional)",
67
+ "role": "reference",
68
+ "asset_type": "image",
69
+ "inputs": [
70
+ {
71
+ "id": "in",
72
+ "label": "Image",
73
+ "type": "image"
74
+ }
75
+ ],
76
+ "outputs": [
77
+ {
78
+ "id": "out",
79
+ "label": "Image",
80
+ "type": "image"
81
+ }
82
+ ],
83
+ "x": 40,
84
+ "y": 370,
85
+ "width": 220,
86
+ "height": 120,
87
+ "data": {}
88
+ },
89
+ {
90
+ "id": "ref_canvas",
91
+ "label": "Canvas",
92
+ "role": "reference",
93
+ "asset_type": "text",
94
+ "inputs": [
95
+ {
96
+ "id": "in",
97
+ "label": "Canvas label",
98
+ "type": "text"
99
+ }
100
+ ],
101
+ "outputs": [
102
+ {
103
+ "id": "out",
104
+ "label": "Canvas",
105
+ "type": "text",
106
+ "choices": [
107
+ "960x544 · 16:9 fast",
108
+ "1024x576 · 16:9 fast",
109
+ "1152x640 · 16:9",
110
+ "1280x704 · 16:9",
111
+ "1344x768 · 16:9 full",
112
+ "544x960 · 9:16 fast",
113
+ "640x1152 · 9:16",
114
+ "768x1344 · 9:16 full",
115
+ "544x544 · 1:1 fast",
116
+ "768x768 · 1:1 full",
117
+ "768x576 · 4:3 fast",
118
+ "1024x768 · 4:3 full",
119
+ "576x768 · 3:4 fast",
120
+ "768x1024 · 3:4 full",
121
+ "1152x512 · 21:9 fast",
122
+ "1536x672 · 21:9 full"
123
+ ]
124
+ }
125
+ ],
126
+ "x": 40,
127
+ "y": 520,
128
+ "width": 220,
129
+ "height": 90,
130
+ "data": {
131
+ "out": "960x544 · 16:9 fast"
132
+ }
133
+ },
134
+ {
135
+ "id": "ref_duration",
136
+ "label": "Duration (s)",
137
+ "role": "reference",
138
+ "asset_type": "number",
139
+ "inputs": [
140
+ {
141
+ "id": "in",
142
+ "label": "Seconds",
143
+ "type": "number"
144
+ }
145
+ ],
146
+ "outputs": [
147
+ {
148
+ "id": "out",
149
+ "label": "Duration",
150
+ "type": "number"
151
+ }
152
+ ],
153
+ "x": 40,
154
+ "y": 630,
155
+ "width": 200,
156
+ "height": 90,
157
+ "data": {
158
+ "out": 5
159
+ }
160
+ },
161
+ {
162
+ "id": "ref_steps",
163
+ "label": "Steps",
164
+ "role": "reference",
165
+ "asset_type": "number",
166
+ "inputs": [
167
+ {
168
+ "id": "in",
169
+ "label": "Steps",
170
+ "type": "number"
171
+ }
172
+ ],
173
+ "outputs": [
174
+ {
175
+ "id": "out",
176
+ "label": "Steps",
177
+ "type": "number",
178
+ "choices": [
179
+ 4,
180
+ 6,
181
+ 28
182
+ ]
183
+ }
184
+ ],
185
+ "x": 40,
186
+ "y": 740,
187
+ "width": 200,
188
+ "height": 90,
189
+ "data": {
190
+ "out": 6
191
+ },
192
+ "steps": null
193
+ },
194
+ {
195
+ "id": "ref_seed",
196
+ "label": "Seed",
197
+ "role": "reference",
198
+ "asset_type": "number",
199
+ "inputs": [
200
+ {
201
+ "id": "in",
202
+ "label": "Seed",
203
+ "type": "number"
204
+ }
205
+ ],
206
+ "outputs": [
207
+ {
208
+ "id": "out",
209
+ "label": "Seed",
210
+ "type": "number"
211
+ }
212
+ ],
213
+ "x": 40,
214
+ "y": 850,
215
+ "width": 200,
216
+ "height": 90,
217
+ "data": {
218
+ "out": 42
219
+ }
220
+ },
221
+ {
222
+ "id": "ref_upsample",
223
+ "label": "Upsample Prompt",
224
+ "role": "reference",
225
+ "asset_type": "boolean",
226
+ "inputs": [
227
+ {
228
+ "id": "in",
229
+ "label": "Upsample",
230
+ "type": "boolean"
231
+ }
232
+ ],
233
+ "outputs": [
234
+ {
235
+ "id": "out",
236
+ "label": "Upsample",
237
+ "type": "boolean"
238
+ }
239
+ ],
240
+ "x": 40,
241
+ "y": 960,
242
+ "width": 200,
243
+ "height": 90,
244
+ "data": {
245
+ "out": false
246
+ }
247
+ },
248
+ {
249
+ "id": "ref_lora",
250
+ "label": "LoRA",
251
+ "role": "reference",
252
+ "asset_type": "text",
253
+ "inputs": [
254
+ {
255
+ "id": "in",
256
+ "label": "LoRA",
257
+ "type": "text"
258
+ }
259
+ ],
260
+ "outputs": [
261
+ {
262
+ "id": "out",
263
+ "label": "LoRA",
264
+ "type": "text",
265
+ "choices": [
266
+ "larry",
267
+ "lightx",
268
+ "off"
269
+ ]
270
+ }
271
+ ],
272
+ "x": 40,
273
+ "y": 1070,
274
+ "width": 260,
275
+ "height": 90,
276
+ "data": {
277
+ "out": "larry"
278
+ }
279
+ }
280
+ ],
281
+ "operators": [
282
+ {
283
+ "id": "op_generate",
284
+ "label": "generate_video",
285
+ "role": "operator",
286
+ "kind": "fn",
287
+ "source": "fn",
288
+ "fn": "generate_video",
289
+ "inputs": [
290
+ {
291
+ "id": "in_0",
292
+ "label": "prompt",
293
+ "type": "text",
294
+ "required": true
295
+ },
296
+ {
297
+ "id": "in_1",
298
+ "label": "first_frame",
299
+ "type": "image"
300
+ },
301
+ {
302
+ "id": "in_2",
303
+ "label": "last_frame",
304
+ "type": "image"
305
+ },
306
+ {
307
+ "id": "in_3",
308
+ "label": "canvas",
309
+ "type": "text",
310
+ "choices": [
311
+ "960x544 · 16:9 fast",
312
+ "1024x576 · 16:9 fast",
313
+ "1152x640 · 16:9",
314
+ "1280x704 · 16:9",
315
+ "1344x768 · 16:9 full",
316
+ "544x960 · 9:16 fast",
317
+ "640x1152 · 9:16",
318
+ "768x1344 · 9:16 full",
319
+ "544x544 · 1:1 fast",
320
+ "768x768 · 1:1 full",
321
+ "768x576 · 4:3 fast",
322
+ "1024x768 · 4:3 full",
323
+ "576x768 · 3:4 fast",
324
+ "768x1024 · 3:4 full",
325
+ "1152x512 · 21:9 fast",
326
+ "1536x672 · 21:9 full"
327
+ ]
328
+ },
329
+ {
330
+ "id": "in_4",
331
+ "label": "duration",
332
+ "type": "number"
333
+ },
334
+ {
335
+ "id": "in_5",
336
+ "label": "steps",
337
+ "type": "number"
338
+ },
339
+ {
340
+ "id": "in_6",
341
+ "label": "seed",
342
+ "type": "number"
343
+ },
344
+ {
345
+ "id": "in_7",
346
+ "label": "upsample",
347
+ "type": "boolean"
348
+ },
349
+ {
350
+ "id": "in_8",
351
+ "label": "lora",
352
+ "type": "text",
353
+ "choices": [
354
+ "larry",
355
+ "lightx",
356
+ "off"
357
+ ]
358
+ }
359
+ ],
360
+ "outputs": [
361
+ {
362
+ "id": "out_0",
363
+ "label": "video",
364
+ "type": "video",
365
+ "output_index": 0
366
+ },
367
+ {
368
+ "id": "out_1",
369
+ "label": "report",
370
+ "type": "text",
371
+ "output_index": 1
372
+ },
373
+ {
374
+ "id": "out_2",
375
+ "label": "refined_prompt",
376
+ "type": "text",
377
+ "output_index": 2
378
+ }
379
+ ],
380
+ "x": 460,
381
+ "y": 420,
382
+ "width": 300,
383
+ "height": 300,
384
+ "data": {}
385
+ }
386
+ ],
387
+ "subjects": [
388
+ {
389
+ "id": "sub_video",
390
+ "label": "Output Video",
391
+ "role": "subject",
392
+ "asset_type": "video",
393
+ "inputs": [
394
+ {
395
+ "id": "in",
396
+ "label": "Video",
397
+ "type": "video"
398
+ }
399
+ ],
400
+ "outputs": [
401
+ {
402
+ "id": "out",
403
+ "label": "Video",
404
+ "type": "video"
405
+ }
406
+ ],
407
+ "x": 880,
408
+ "y": 380,
409
+ "width": 280,
410
+ "height": 160,
411
+ "data": {}
412
+ },
413
+ {
414
+ "id": "sub_report",
415
+ "label": "Report",
416
+ "role": "subject",
417
+ "asset_type": "text",
418
+ "inputs": [
419
+ {
420
+ "id": "in",
421
+ "label": "Report",
422
+ "type": "text"
423
+ }
424
+ ],
425
+ "outputs": [
426
+ {
427
+ "id": "out",
428
+ "label": "Report",
429
+ "type": "text"
430
+ }
431
+ ],
432
+ "x": 880,
433
+ "y": 580,
434
+ "width": 280,
435
+ "height": 110,
436
+ "data": {}
437
+ },
438
+ {
439
+ "id": "sub_refined",
440
+ "label": "Refined Prompt",
441
+ "role": "subject",
442
+ "asset_type": "text",
443
+ "inputs": [
444
+ {
445
+ "id": "in",
446
+ "label": "Refined prompt",
447
+ "type": "text"
448
+ }
449
+ ],
450
+ "outputs": [
451
+ {
452
+ "id": "out",
453
+ "label": "Refined prompt",
454
+ "type": "text"
455
+ }
456
+ ],
457
+ "x": 880,
458
+ "y": 720,
459
+ "width": 280,
460
+ "height": 110,
461
+ "data": {}
462
+ }
463
+ ],
464
+ "edges": [
465
+ {
466
+ "id": "e_prompt",
467
+ "from_node_id": "ref_prompt",
468
+ "from_port_id": "out",
469
+ "to_node_id": "op_generate",
470
+ "to_port_id": "in_0",
471
+ "type": "text"
472
+ },
473
+ {
474
+ "id": "e_first",
475
+ "from_node_id": "ref_first",
476
+ "from_port_id": "out",
477
+ "to_node_id": "op_generate",
478
+ "to_port_id": "in_1",
479
+ "type": "image"
480
+ },
481
+ {
482
+ "id": "e_last",
483
+ "from_node_id": "ref_last",
484
+ "from_port_id": "out",
485
+ "to_node_id": "op_generate",
486
+ "to_port_id": "in_2",
487
+ "type": "image"
488
+ },
489
+ {
490
+ "id": "e_canvas",
491
+ "from_node_id": "ref_canvas",
492
+ "from_port_id": "out",
493
+ "to_node_id": "op_generate",
494
+ "to_port_id": "in_3",
495
+ "type": "text"
496
+ },
497
+ {
498
+ "id": "e_duration",
499
+ "from_node_id": "ref_duration",
500
+ "from_port_id": "out",
501
+ "to_node_id": "op_generate",
502
+ "to_port_id": "in_4",
503
+ "type": "number"
504
+ },
505
+ {
506
+ "id": "e_steps",
507
+ "from_node_id": "ref_steps",
508
+ "from_port_id": "out",
509
+ "to_node_id": "op_generate",
510
+ "to_port_id": "in_5",
511
+ "type": "number"
512
+ },
513
+ {
514
+ "id": "e_seed",
515
+ "from_node_id": "ref_seed",
516
+ "from_port_id": "out",
517
+ "to_node_id": "op_generate",
518
+ "to_port_id": "in_6",
519
+ "type": "number"
520
+ },
521
+ {
522
+ "id": "e_upsample",
523
+ "from_node_id": "ref_upsample",
524
+ "from_port_id": "out",
525
+ "to_node_id": "op_generate",
526
+ "to_port_id": "in_7",
527
+ "type": "boolean"
528
+ },
529
+ {
530
+ "id": "e_lora",
531
+ "from_node_id": "ref_lora",
532
+ "from_port_id": "out",
533
+ "to_node_id": "op_generate",
534
+ "to_port_id": "in_8",
535
+ "type": "text"
536
+ },
537
+ {
538
+ "id": "e_video_out",
539
+ "from_node_id": "op_generate",
540
+ "from_port_id": "out_0",
541
+ "to_node_id": "sub_video",
542
+ "to_port_id": "in",
543
+ "type": "video"
544
+ },
545
+ {
546
+ "id": "e_report_out",
547
+ "from_node_id": "op_generate",
548
+ "from_port_id": "out_1",
549
+ "to_node_id": "sub_report",
550
+ "to_port_id": "in",
551
+ "type": "text"
552
+ },
553
+ {
554
+ "id": "e_refined_out",
555
+ "from_node_id": "op_generate",
556
+ "from_port_id": "out_2",
557
+ "to_node_id": "sub_refined",
558
+ "to_port_id": "in",
559
+ "type": "text"
560
+ }
561
+ ]
562
+ }