prithivMLmods commited on
Commit
a8b2d88
·
verified ·
1 Parent(s): bd66bda

update app

Browse files
Files changed (1) hide show
  1. app.py +162 -73
app.py CHANGED
@@ -1,3 +1,10 @@
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
  import math
@@ -7,16 +14,10 @@ import subprocess
7
  import tempfile
8
  import time
9
  import traceback
10
- import base64
11
- import json
12
  from functools import cache
13
- from io import BytesIO
14
- from PIL import Image
15
 
16
  import spaces
17
  import gradio as gr
18
- from gradio import Server
19
- from fastapi.responses import HTMLResponse
20
 
21
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
22
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
@@ -52,8 +53,9 @@ PIPELINE_MAX_DURATION = MAX_UI_DURATION + 1.0
52
  CHUNK_SECONDS = float(os.environ.get("H3_CHUNK_SECONDS", "14"))
53
  CONDITIONER_MAX_FRAMES = int(os.environ.get("H3_CONDITIONER_MAX_FRAMES", "345"))
54
 
 
55
  GPU_DURATION_CHOICES = (60, 90, 120, 150, 180, 250)
56
- DEFAULT_GPU_DURATION = 120
57
 
58
  OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "h3-outputs")
59
 
@@ -75,6 +77,7 @@ def snap_frames(seconds: float) -> int:
75
 
76
  def patch_duration_window(minimum: float, maximum: float) -> None:
77
  from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
 
78
  MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(minimum))
79
  MiniMaxH3ModularPipeline.max_duration = property(lambda self: float(maximum))
80
 
@@ -82,17 +85,19 @@ def patch_duration_window(minimum: float, maximum: float) -> None:
82
  def _apply_lora(transformer) -> str | None:
83
  try:
84
  import h3_lora
 
85
  return h3_lora.apply_lora(transformer)
86
  except ImportError:
87
- return "⚠️ `h3_lora.py` is missing — running without the Turbo LoRA."
88
  except Exception as error:
89
  traceback.print_exc()
90
- return f"⚠️ LoRA loading failed ({type(error).__name__}: {error})"
91
 
92
 
93
  def _set_lora(transformer, name: str) -> str:
94
  try:
95
  import h3_lora
 
96
  return h3_lora.set_active(transformer, name)
97
  except Exception:
98
  return "off"
@@ -100,6 +105,7 @@ def _set_lora(transformer, name: str) -> str:
100
 
101
  def load_models() -> str | None:
102
  global PIPE, MUTE_PIPE, MANAGER, LOAD_ERROR, MUTE_ERROR, LOADED_IN, LORA_STATUS
 
103
  if PIPE is not None or LOAD_ERROR is not None:
104
  return LOAD_ERROR
105
 
@@ -107,6 +113,7 @@ def load_models() -> str | None:
107
  try:
108
  import torch
109
  from diffusers import ComponentsManager
 
110
  from h3_split_blocks import MiniMaxH3GeneratorBlocks
111
 
112
  patch_duration_window(MIN_UI_DURATION, PIPELINE_MAX_DURATION)
@@ -144,6 +151,7 @@ def load_models() -> str | None:
144
  def _build_mute_pipe(pipe):
145
  try:
146
  from h3_split_blocks import MiniMaxH3MuteGeneratorBlocks
 
147
  blocks = MiniMaxH3MuteGeneratorBlocks()
148
  mute = blocks.init_pipeline(MODEL_REPO)
149
  shared = {}
@@ -178,6 +186,7 @@ def _arm_decode_hooks(pipe):
178
  @cache
179
  def conditioner():
180
  from gradio_client import Client
 
181
  return Client(CONDITIONER_SPACE)
182
 
183
 
@@ -185,6 +194,7 @@ def conditioner_client(ip_token):
185
  if not ip_token:
186
  return conditioner()
187
  from gradio_client import Client
 
188
  return Client(CONDITIONER_SPACE, headers={"x-ip-token": ip_token})
189
 
190
 
@@ -228,8 +238,13 @@ def estimate_seconds(height, width, num_frames, steps, num_keyframes=0, with_aud
228
 
229
  def get_duration(
230
  prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
231
- lora="lightx", with_audio=True, gpu_duration=DEFAULT_GPU_DURATION, *a, **k
232
  ):
 
 
 
 
 
233
  if gpu_duration:
234
  try:
235
  value = int(gpu_duration)
@@ -244,10 +259,12 @@ def get_duration(
244
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
245
  def _generate(
246
  prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
247
- lora="lightx", with_audio=True, gpu_duration=DEFAULT_GPU_DURATION,
248
  ):
249
  import torch
 
250
  active_lora = _set_lora(PIPE.transformer, lora)
 
251
  pipe = PIPE if (with_audio or MUTE_PIPE is None) else MUTE_PIPE
252
  decoded_audio = with_audio or MUTE_PIPE is None
253
 
@@ -277,6 +294,7 @@ def _generate(
277
 
278
  def _fit_keyframe(image_path, current_canvas):
279
  from PIL import Image as _Image
 
280
  img = _Image.open(image_path)
281
  aspect = img.width / img.height
282
  fastest = {}
@@ -311,7 +329,7 @@ LORA_NAMES = ("larry", "lightx", "lightx8", "realism", "joyfox", "H3-Facial-Real
311
 
312
  def _resolve_lora(lora) -> str:
313
  if not isinstance(lora, str) or not lora.strip():
314
- return "lightx"
315
  value = lora.strip().lower()
316
  lowered = {name.lower(): name for name in LORA_NAMES}
317
  if value in lowered:
@@ -322,8 +340,15 @@ def _resolve_lora(lora) -> str:
322
  raise gr.Error(f"Unknown LoRA `{lora}`. Allowed: {', '.join(LORA_NAMES)}.")
323
 
324
 
 
 
 
 
 
 
325
  def keyframe(path):
326
  from PIL import Image, ImageOps
 
327
  return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
328
 
329
 
@@ -332,6 +357,7 @@ def _render_clip(prompt, first, last, canvas, duration, steps, seed, upsample, l
332
  from diffusers.utils import encode_video
333
 
334
  num_frames = snap_frames(duration)
 
335
  conditioned = time.time()
336
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
337
  prompt, first, last, canvas, num_frames, rewrite_prompt=upsample, ip_token=ip_token
@@ -399,8 +425,8 @@ CHAIN_SUFFIX = (
399
 
400
 
401
  def generate(
402
- prompt, first_frame=None, last_frame=None, canvas=DEFAULT_CANVAS, duration=3.0, mode="single",
403
- steps=5, seed=42, upsample=False, lora="lightx", with_audio=True, ip_token=None, progress=None,
404
  gpu_duration=DEFAULT_GPU_DURATION,
405
  ):
406
  if LOAD_ERROR:
@@ -416,6 +442,7 @@ def generate(
416
  duration = max(MIN_UI_DURATION, min(float(duration), MAX_UI_DURATION))
417
  with_audio = bool(with_audio)
418
 
 
419
  try:
420
  gpu_duration = int(gpu_duration)
421
  except (TypeError, ValueError):
@@ -423,7 +450,7 @@ def generate(
423
  if gpu_duration not in GPU_DURATION_CHOICES:
424
  gpu_duration = min(GPU_DURATION_CHOICES, key=lambda v: abs(v - gpu_duration))
425
 
426
- first, last = first_frame, last_frame
427
  if first:
428
  first, canvas = _fit_keyframe(first, canvas)
429
  if last:
@@ -482,67 +509,129 @@ def generate(
482
  shutil.rmtree(work, ignore_errors=True)
483
 
484
 
485
- def save_b64_image(b64_str):
486
- if not b64_str: return None
487
- if "," in b64_str:
488
- _, data = b64_str.split(",", 1)
489
- else:
490
- data = b64_str
491
- img_data = base64.b64decode(data)
492
- temp_file = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
493
- temp_file.write(img_data)
494
- temp_file.close()
495
- return temp_file.name
496
-
497
- # ── Gradio Server (Server mode): FastAPI + Gradio queue/API engine ────────────
498
- app = Server(title="MiniMax-H3-Turbo-LoRA-Fast")
499
-
500
- @app.mcp.tool(name="generate_video")
501
- @app.api(name="generate_video")
502
- def infer(
503
- prompt: str,
504
- images_b64_json: str,
505
- canvas: str,
506
- duration: float,
507
- mode: str,
508
- steps: int,
509
- seed: int,
510
- upsample: bool,
511
- lora: str,
512
- with_audio: bool,
513
- gpu_duration: int,
514
- ) -> dict:
515
- """Generates a video using MiniMax-H3-Turbo-LoRA-Fast."""
516
- images = json.loads(images_b64_json) if images_b64_json else []
517
- first_frame = save_b64_image(images[0]) if len(images) > 0 else None
518
- last_frame = save_b64_image(images[1]) if len(images) > 1 else None
519
-
520
- final_video_path, summary, refined = generate(
521
  prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio,
522
- ip_token=None, progress=None, gpu_duration=gpu_duration
523
  )
524
-
525
- with open(final_video_path, "rb") as f:
526
- video_b64 = base64.b64encode(f.read()).decode()
527
- os.remove(final_video_path)
528
-
529
- return {
530
- "video": f"data:video/mp4;base64,{video_b64}",
531
- "report": summary,
532
- "refined": refined
533
- }
534
-
535
- @app.get("/api/config")
536
- def client_config():
537
- return {"examples": []}
538
-
539
- @app.get("/", response_class=HTMLResponse)
540
- async def homepage():
541
- html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
542
- with open(html_path, "r", encoding="utf-8") as f:
543
- return f.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
  load_models()
546
 
547
  if __name__ == "__main__":
548
- app.launch(show_error=True, mcp_server=True, allowed_paths=[OUTPUT_DIR])
 
1
+ """MiniMax-H3-Turbo-LoRA-Fast — the denoising half of a split MiniMax-H3 deployment.
2
+
3
+ The 62 GiB Qwen3-VL text encoder runs in a separate conditioner Space; this Space loads the transformer and the
4
+ VAEs, folds the selected Turbo/Style LoRA into the bf16 weights, and serves first/last-frame video generation up
5
+ to 20 seconds with optional audio.
6
+ """
7
+
8
  from __future__ import annotations
9
 
10
  import math
 
14
  import tempfile
15
  import time
16
  import traceback
 
 
17
  from functools import cache
 
 
18
 
19
  import spaces
20
  import gradio as gr
 
 
21
 
22
  MODEL_REPO = os.environ.get("H3_MODEL_REPO", "MiniMaxAI/MiniMax-H3")
23
  CONDITIONER_SPACE = os.environ.get("H3_CONDITIONER", "multimodalart/qwen3vl-conditioner")
 
53
  CHUNK_SECONDS = float(os.environ.get("H3_CHUNK_SECONDS", "14"))
54
  CONDITIONER_MAX_FRAMES = int(os.environ.get("H3_CONDITIONER_MAX_FRAMES", "345"))
55
 
56
+ # Allowed GPU-duration choices (seconds) exposed in the Advanced panel.
57
  GPU_DURATION_CHOICES = (60, 90, 120, 150, 180, 250)
58
+ DEFAULT_GPU_DURATION = 90
59
 
60
  OUTPUT_DIR = os.path.join(tempfile.gettempdir(), "h3-outputs")
61
 
 
77
 
78
  def patch_duration_window(minimum: float, maximum: float) -> None:
79
  from diffusers.modular_pipelines.minimax_h3.modular_pipeline import MiniMaxH3ModularPipeline
80
+
81
  MiniMaxH3ModularPipeline.min_duration = property(lambda self: float(minimum))
82
  MiniMaxH3ModularPipeline.max_duration = property(lambda self: float(maximum))
83
 
 
85
  def _apply_lora(transformer) -> str | None:
86
  try:
87
  import h3_lora
88
+
89
  return h3_lora.apply_lora(transformer)
90
  except ImportError:
91
+ return "⚠️ `h3_lora.py` is missing — running without the Turbo LoRA. Raise the steps or add the file (README)"
92
  except Exception as error:
93
  traceback.print_exc()
94
+ return f"⚠️ LoRA loading failed ({type(error).__name__}: {error}) — running without"
95
 
96
 
97
  def _set_lora(transformer, name: str) -> str:
98
  try:
99
  import h3_lora
100
+
101
  return h3_lora.set_active(transformer, name)
102
  except Exception:
103
  return "off"
 
105
 
106
  def load_models() -> str | None:
107
  global PIPE, MUTE_PIPE, MANAGER, LOAD_ERROR, MUTE_ERROR, LOADED_IN, LORA_STATUS
108
+
109
  if PIPE is not None or LOAD_ERROR is not None:
110
  return LOAD_ERROR
111
 
 
113
  try:
114
  import torch
115
  from diffusers import ComponentsManager
116
+
117
  from h3_split_blocks import MiniMaxH3GeneratorBlocks
118
 
119
  patch_duration_window(MIN_UI_DURATION, PIPELINE_MAX_DURATION)
 
151
  def _build_mute_pipe(pipe):
152
  try:
153
  from h3_split_blocks import MiniMaxH3MuteGeneratorBlocks
154
+
155
  blocks = MiniMaxH3MuteGeneratorBlocks()
156
  mute = blocks.init_pipeline(MODEL_REPO)
157
  shared = {}
 
186
  @cache
187
  def conditioner():
188
  from gradio_client import Client
189
+
190
  return Client(CONDITIONER_SPACE)
191
 
192
 
 
194
  if not ip_token:
195
  return conditioner()
196
  from gradio_client import Client
197
+
198
  return Client(CONDITIONER_SPACE, headers={"x-ip-token": ip_token})
199
 
200
 
 
238
 
239
  def get_duration(
240
  prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
241
+ lora="larry", with_audio=True, gpu_duration=DEFAULT_GPU_DURATION, *a, **k
242
  ):
243
+ """Duration callback for `@spaces.GPU`.
244
+
245
+ Honors an explicit user override (`gpu_duration`) when it is one of the
246
+ allowed choices; otherwise falls back to the empirical estimate.
247
+ """
248
  if gpu_duration:
249
  try:
250
  value = int(gpu_duration)
 
259
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
260
  def _generate(
261
  prompt_embeds, text_token_tags, image, last_image, height, width, num_frames, steps, seed,
262
+ lora="larry", with_audio=True, gpu_duration=DEFAULT_GPU_DURATION,
263
  ):
264
  import torch
265
+
266
  active_lora = _set_lora(PIPE.transformer, lora)
267
+
268
  pipe = PIPE if (with_audio or MUTE_PIPE is None) else MUTE_PIPE
269
  decoded_audio = with_audio or MUTE_PIPE is None
270
 
 
294
 
295
  def _fit_keyframe(image_path, current_canvas):
296
  from PIL import Image as _Image
297
+
298
  img = _Image.open(image_path)
299
  aspect = img.width / img.height
300
  fastest = {}
 
329
 
330
  def _resolve_lora(lora) -> str:
331
  if not isinstance(lora, str) or not lora.strip():
332
+ return "larry"
333
  value = lora.strip().lower()
334
  lowered = {name.lower(): name for name in LORA_NAMES}
335
  if value in lowered:
 
340
  raise gr.Error(f"Unknown LoRA `{lora}`. Allowed: {', '.join(LORA_NAMES)}.")
341
 
342
 
343
+ def _as_path(value):
344
+ if isinstance(value, dict):
345
+ value = value.get("path") or (value.get("url") or "").removeprefix("/gradio_api/file=")
346
+ return value or None
347
+
348
+
349
  def keyframe(path):
350
  from PIL import Image, ImageOps
351
+
352
  return ImageOps.exif_transpose(Image.open(path)).convert("RGB") if path else None
353
 
354
 
 
357
  from diffusers.utils import encode_video
358
 
359
  num_frames = snap_frames(duration)
360
+
361
  conditioned = time.time()
362
  prompt_embeds, text_token_tags, metadata, plan = encode_remote(
363
  prompt, first, last, canvas, num_frames, rewrite_prompt=upsample, ip_token=ip_token
 
425
 
426
 
427
  def generate(
428
+ prompt, first_frame=None, last_frame=None, canvas=DEFAULT_CANVAS, duration=5.0, mode="single",
429
+ steps=6, seed=42, upsample=False, lora="larry", with_audio=True, ip_token=None, progress=None,
430
  gpu_duration=DEFAULT_GPU_DURATION,
431
  ):
432
  if LOAD_ERROR:
 
442
  duration = max(MIN_UI_DURATION, min(float(duration), MAX_UI_DURATION))
443
  with_audio = bool(with_audio)
444
 
445
+ # Normalize GPU-duration override to one of the allowed choices.
446
  try:
447
  gpu_duration = int(gpu_duration)
448
  except (TypeError, ValueError):
 
450
  if gpu_duration not in GPU_DURATION_CHOICES:
451
  gpu_duration = min(GPU_DURATION_CHOICES, key=lambda v: abs(v - gpu_duration))
452
 
453
+ first, last = _as_path(first_frame), _as_path(last_frame)
454
  if first:
455
  first, canvas = _fit_keyframe(first, canvas)
456
  if last:
 
509
  shutil.rmtree(work, ignore_errors=True)
510
 
511
 
512
+ def _caller_ip_token(request: gr.Request | None) -> str | None:
513
+ if request is None:
514
+ return None
515
+ headers = getattr(request, "headers", None)
516
+ return headers.get("x-ip-token") if headers is not None else None
517
+
518
+
519
+ def run(prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio,
520
+ gpu_duration, request: gr.Request = None, progress=gr.Progress()):
521
+ return generate(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio,
523
+ ip_token=_caller_ip_token(request), progress=progress, gpu_duration=gpu_duration,
524
  )
525
+
526
+
527
+ def cost_hint(canvas, duration, mode, steps, first_frame, last_frame, with_audio, gpu_duration):
528
+ try:
529
+ height, width = CANVASES.get(canvas, CANVASES[DEFAULT_CANVAS])
530
+ keyframes = int(bool(first_frame)) + int(bool(last_frame))
531
+ durations = [float(duration)] if mode == "single" else plan_clips(float(duration), CHUNK_SECONDS)
532
+ estimated = sum(
533
+ estimate_seconds(height, width, snap_frames(d), int(steps), keyframes, bool(with_audio))
534
+ for d in durations
535
+ )
536
+ try:
537
+ gd = int(gpu_duration)
538
+ except (TypeError, ValueError):
539
+ gd = DEFAULT_GPU_DURATION
540
+ booking = gd * len(durations) if gd in GPU_DURATION_CHOICES else estimated
541
+ frames = sum(snap_frames(d) for d in durations)
542
+ parts = [
543
+ f"{width}x{height}",
544
+ f"{frames} frames ({frames / FPS:.2f}s)",
545
+ f"{len(durations)} pass(es)",
546
+ f"~{booking}s GPU booking" + (f" ({gd}s/clip override)" if gd in GPU_DURATION_CHOICES else " (auto-estimate)"),
547
+ ]
548
+ if not with_audio:
549
+ parts.append("audio VAE stays on the host")
550
+ if mode == "single" and float(duration) > 15:
551
+ parts.append("⚠️ >15s extrapolates")
552
+ return " · ".join(parts)
553
+ except Exception as error:
554
+ return f"(estimate unavailable: {error})"
555
+
556
+
557
+ INTRO = f"""# MiniMax-H3-Turbo-LoRA-Fast
558
+
559
+ Denoising half of a split deployment: the 62 GiB Qwen3-VL text encoder runs in
560
+ [`{CONDITIONER_SPACE}`](https://huggingface.co/spaces/{CONDITIONER_SPACE}); this Space holds only the transformer
561
+ and the VAEs.
562
+
563
+ * **First/Last Frame** — both optional and usable individually. A last frame alone means "generate *towards* this image".
564
+ * **Length** — `single` is one pass with a raised ceiling (H3 is trained up to 15 s; beyond that is experimental).
565
+ `chain` cuts the video into clips of at most {CHUNK_SECONDS:g}s and hands the last frame of each clip to the next.
566
+ * **Audio** — when disabled, the audio VAE is never declared, never moved to the GPU and never decoded. The audio
567
+ rows themselves are always denoised by H3; they are part of the same packed sequence.
568
+ * **GPU Duration** — override the ZeroGPU booking per clip in the Advanced panel
569
+ (choices: {', '.join(str(v) for v in GPU_DURATION_CHOICES)} s; default {DEFAULT_GPU_DURATION} s).
570
+ """
571
+
572
+
573
+ with gr.Blocks(title="MiniMax-H3-Turbo-LoRA-Fast") as demo:
574
+ gr.Markdown(INTRO)
575
+
576
+ with gr.Row():
577
+ with gr.Column(scale=3):
578
+ prompt = gr.Textbox(
579
+ label="Prompt",
580
+ lines=4,
581
+ value="An extreme close-up shot focuses on a young Asian woman with auburn-tinted hair and silver ear piercings, gazing intensely toward the camera. She subtly blinks and shifts her deep brown eyes with a contemplative expression as soft, warm ambient light highlights fine skin texture, natural blemishes, and delicate facial micro-movements.",
582
+ )
583
+ with gr.Row():
584
+ first_frame = gr.Image(label="First Frame (optional)", type="filepath", height=200)
585
+ last_frame = gr.Image(label="Last Frame (optional)", type="filepath", height=200)
586
+ with gr.Row():
587
+ canvas = gr.Dropdown(label="Canvas", choices=list(CANVASES), value=DEFAULT_CANVAS)
588
+ mode = gr.Radio(
589
+ label="Duration mode",
590
+ choices=[("single (1 pass)", "single"), (f"chain (clips ≤ {CHUNK_SECONDS:g}s)", "chain")],
591
+ value="single",
592
+ )
593
+ duration = gr.Slider(
594
+ MIN_UI_DURATION, MAX_UI_DURATION, value=4, step=0.5,
595
+ label="Length (s)", info="Rounded up to the next 17n+5 at 24 fps.",
596
+ )
597
+ with gr.Row():
598
+ with_audio = gr.Checkbox(label="Generate audio", value=False)
599
+ upsample = gr.Checkbox(label="Prompt upsampling (+15–50s)", value=False)
600
+ with gr.Accordion("Advanced", open=True):
601
+ steps = gr.Slider(2, 12, value=6, step=1, label="Steps")
602
+ lora = gr.Dropdown(label="Turbo/Style-LoRA", choices=list(LORA_NAMES), value="lightx")
603
+ seed = gr.Number(label="Seed (clip i = seed + i)", value=42, precision=0)
604
+ gpu_duration = gr.Slider(
605
+ minimum=GPU_DURATION_CHOICES[0],
606
+ maximum=GPU_DURATION_CHOICES[-1],
607
+ value=DEFAULT_GPU_DURATION,
608
+ step=10,
609
+ label="GPU Duration (s per clip)",
610
+ info=f"Allowed: {', '.join(str(v) for v in GPU_DURATION_CHOICES)} s · "
611
+ f"default {DEFAULT_GPU_DURATION} s · overrides ZeroGPU booking per clip.",
612
+ )
613
+ cost = gr.Markdown()
614
+ run_button = gr.Button("Generate video", variant="primary")
615
+
616
+ with gr.Column(scale=2):
617
+ output_video = gr.Video(label="Result", format="mp4", autoplay=True)
618
+ report = gr.Markdown(label="Report")
619
+ refined = gr.Textbox(label="Refined Prompt", lines=4)
620
+
621
+ cost_inputs = [canvas, duration, mode, steps, first_frame, last_frame, with_audio, gpu_duration]
622
+ for control in cost_inputs:
623
+ control.change(cost_hint, cost_inputs, cost)
624
+ demo.load(cost_hint, cost_inputs, cost)
625
+
626
+ run_button.click(
627
+ run,
628
+ [prompt, first_frame, last_frame, canvas, duration, mode, steps, seed, upsample, lora, with_audio, gpu_duration],
629
+ [output_video, report, refined],
630
+ api_name="generate",
631
+ )
632
+
633
 
634
  load_models()
635
 
636
  if __name__ == "__main__":
637
+ demo.queue(default_concurrency_limit=1, max_size=8).launch(theme=gr.themes.Citrus(), show_error=True, allowed_paths=[OUTPUT_DIR])