dagloop5 commited on
Commit
2e2333e
·
verified ·
1 Parent(s): 2070802

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -24
app.py CHANGED
@@ -91,6 +91,9 @@ SCHEDULES = {
91
  DEFAULT_SCHEDULE = "linear_quadratic · PlagueKind"
92
  DEFAULT_VIDEO_SHIFT = 12.0
93
  DEFAULT_AUDIO_SHIFT = 3.0
 
 
 
94
 
95
  FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5
96
  # It is the *snapped* frame count the ceiling has to hold for: 15 s is 360 frames, which rounds up to 362, i.e.
@@ -113,7 +116,12 @@ STEP_LINEAR, STEP_QUADRATIC, SAFETY = 1.1745e-4, 3.8396e-9, 1.3
113
  PLACEMENT_ALLOWANCE = int(os.environ.get("H3_PLACEMENT_ALLOWANCE", "8"))
114
  AUDIO_LATENTS_PER_SECOND, AUDIO_CHANNELS = 40, 2
115
  REFERENCE_IMAGE_SHORT_EDGE, CANVAS_MULTIPLE = 2048, 32
116
- DECODE_BASE, DECODE_PER_DEFAULT_CANVAS, DEFAULT_CANVAS_PIXELS = 15, 25, 960 * 544 * 124
 
 
 
 
 
117
 
118
 
119
  def snap_frames(seconds: float) -> int:
@@ -187,7 +195,8 @@ def reference_rows(references: list[tuple[str, str]], num_frames: int) -> int:
187
 
188
 
189
  def get_duration(
190
- prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed, **_
 
191
  ):
192
  """Seconds of GPU to reserve for one request. Takes the arguments of the `@spaces.GPU` function it decorates, and
193
  tolerates the `gr.Progress` `spaces` injects."""
@@ -199,7 +208,16 @@ def get_duration(
199
  # they are handed rather than with the step count.
200
  encode = 5 + reference_rows(references, num_frames) * 1e-3
201
  decode = DECODE_BASE + DECODE_PER_DEFAULT_CANVAS * (height * width * num_frames) / DEFAULT_CANVAS_PIXELS
202
- total = PLACEMENT_ALLOWANCE + encode + denoise + decode + 10
 
 
 
 
 
 
 
 
 
203
  duration = max(MIN_GPU_DURATION, min(MAX_GPU_DURATION, int(total)))
204
  print(f"[ref2va] S={sequence} -> reserving {duration}s ({denoise:.0f}s of denoise at {steps} steps)", flush=True)
205
  return duration
@@ -463,29 +481,62 @@ def encode_remote(prompt, references, canvas, num_frames, rewrite_prompt=False):
463
 
464
 
465
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
466
- def _generate(prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed):
467
- """The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop and the decoders.
468
- References cross as paths and are decoded here; only the three generated outputs come back. A `@spaces.GPU`
469
- argument crosses a process boundary by pickling, a 5 s 1344x768 reference video is 370 MB of expanded frames, and
470
- the full `PipelineState` still holds the packed latents and the rotary grid on the card.
 
 
 
 
471
  """
472
  import torch
 
 
 
473
  if PLACEMENT == "lazy":
474
  PIPE.to("cuda")
475
  elif PLACEMENT == "pack":
476
  PIPE.vae.to("cuda")
477
  PIPE.audio_vae.to("cuda")
478
- state = PIPE(
479
- prompt_embeds=prompt_embeds.to("cuda"),
480
- text_token_tags=text_token_tags,
481
- references=build_references(references),
482
- height=height,
483
- width=width,
484
- num_frames=num_frames,
485
- num_inference_steps=int(steps),
486
- generator=torch.Generator("cpu").manual_seed(int(seed)),
487
- )
488
- return state.get("videos")[0], state.get("audio")[0].cpu(), state.get("sampling_rate")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
 
491
  def generate(
@@ -507,6 +558,12 @@ def generate(
507
  steps=28,
508
  seed=42,
509
  upsample=False,
 
 
 
 
 
 
510
  progress=gr.Progress(track_tqdm=True),
511
  ):
512
  """One request."""
@@ -525,6 +582,9 @@ def generate(
525
  # soundtrack. The conditioner resolves it either way and this Space pins whatever comes back.
526
  derivable = len(audio_bearing(references)) == 1
527
  requested = 0 if (match and derivable) else snap_frames(duration)
 
 
 
528
 
529
  progress(0.0, desc="Upsampling the prompt ..." if upsample else "Reading the prompt and references ...")
530
  conditioned = time.time()
@@ -548,19 +608,21 @@ def generate(
548
 
549
  progress(0.1, desc=f"Generating {num_frames / FPS:.1f} s at {width}x{height} ...")
550
  started = time.time()
551
- frames, audio, sampling_rate = _generate(
552
- prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed
 
553
  )
554
  generate_seconds = time.time() - started
555
 
556
  directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
557
  os.makedirs(directory, exist_ok=True)
558
  path = os.path.join(directory, f"h3-ref2va-{int(time.time() * 1000)}.mp4")
559
- encode_video(frames, fps=FPS, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
560
 
561
  print(
562
  f"[ref2va] {[kind for kind, _ in references]} · `{width}x{height}`, {num_frames} frames "
563
- f"({num_frames / FPS:.3f} s), {int(steps)} steps · conditioner {condition_seconds:.0f}s "
 
564
  f"({plan['num_text_tokens']} tokens{', upsampled' if refined else ''}) · "
565
  f"denoise + decode {generate_seconds:.0f}s "
566
  f"({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}",
@@ -648,6 +710,20 @@ with gr.Blocks(title="MiniMax-H3 Reference Custom Lora") as demo:
648
  audio_shift = gr.Slider(
649
  label="Audio shift", minimum=0.5, maximum=20.0, step=0.5, value=DEFAULT_AUDIO_SHIFT
650
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  seed = gr.Number(label="Seed", value=42, precision=0)
652
 
653
  with gr.Column():
@@ -679,7 +755,8 @@ with gr.Blocks(title="MiniMax-H3 Reference Custom Lora") as demo:
679
  video.change(auto_canvas, video, canvas, show_progress="hidden", api_name=False)
680
 
681
  request = [
682
- prompt, images[0], audio, video, canvas, *images[1:], match, duration, steps, seed, upsample
 
683
  ]
684
  run.click(generate, request, [result, upsampled, upsampled_panel], api_name="generate")
685
 
 
91
  DEFAULT_SCHEDULE = "linear_quadratic · PlagueKind"
92
  DEFAULT_VIDEO_SHIFT = 12.0
93
  DEFAULT_AUDIO_SHIFT = 3.0
94
+ DEFAULT_SHARPEN = 0.3
95
+ INTERPOLATION = {"off · 24 fps": 1, "2x · 48 fps (PlagueKind)": 2, "4x · 96 fps": 4}
96
+ DEFAULT_INTERPOLATION = "2x · 48 fps (PlagueKind)"
97
 
98
  FPS, FRAMES_PER_CHUNK, LATENTS_PER_CHUNK = 24, 17, 5
99
  # It is the *snapped* frame count the ceiling has to hold for: 15 s is 360 frames, which rounds up to 362, i.e.
 
116
  PLACEMENT_ALLOWANCE = int(os.environ.get("H3_PLACEMENT_ALLOWANCE", "8"))
117
  AUDIO_LATENTS_PER_SECOND, AUDIO_CHANNELS = 40, 2
118
  REFERENCE_IMAGE_SHORT_EDGE, CANVAS_MULTIPLE = 2048, 32
119
+ DDECODE_BASE, DECODE_PER_DEFAULT_CANVAS, DEFAULT_CANVAS_PIXELS = 15, 25, 960 * 544 * 124
120
+ # The workflow's post chain. RCAS is a handful of elementwise passes over the clip; FILM is per *emitted*
121
+ # intermediate frame; the h264 mux is per frame actually written. Ported from the fl2va Space's fitted constants
122
+ # as a starting point, same caveat as `PLACEMENT_ALLOWANCE` above — worth checking booked-vs-actual here
123
+ # specifically once this is testable.
124
+ _POST_BASE, _FILM_PER_FRAME, _MUX_PER_FRAME = 2.0, 0.025, 0.02
125
 
126
 
127
  def snap_frames(seconds: float) -> int:
 
195
 
196
 
197
  def get_duration(
198
+ prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed,
199
+ sampler, schedule, video_shift, audio_shift, sharpen, multiplier, **_
200
  ):
201
  """Seconds of GPU to reserve for one request. Takes the arguments of the `@spaces.GPU` function it decorates, and
202
  tolerates the `gr.Progress` `spaces` injects."""
 
208
  # they are handed rather than with the step count.
209
  encode = 5 + reference_rows(references, num_frames) * 1e-3
210
  decode = DECODE_BASE + DECODE_PER_DEFAULT_CANVAS * (height * width * num_frames) / DEFAULT_CANVAS_PIXELS
211
+
212
+ multiplier = max(1, int(multiplier))
213
+ if multiplier > 1 and FILM is None:
214
+ multiplier = 1
215
+ pixel_ratio = (height * width) / (960 * 544)
216
+ out_frames = (num_frames - 1) * multiplier + 1 if multiplier > 1 else num_frames
217
+ film = (num_frames - 1) * (multiplier - 1) * _FILM_PER_FRAME * pixel_ratio
218
+ post = _POST_BASE + film + out_frames * _MUX_PER_FRAME * pixel_ratio
219
+
220
+ total = PLACEMENT_ALLOWANCE + encode + denoise + decode + post
221
  duration = max(MIN_GPU_DURATION, min(MAX_GPU_DURATION, int(total)))
222
  print(f"[ref2va] S={sequence} -> reserving {duration}s ({denoise:.0f}s of denoise at {steps} steps)", flush=True)
223
  return duration
 
481
 
482
 
483
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
484
+ def _generate(
485
+ prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed,
486
+ sampler, schedule, video_shift, audio_shift, sharpen, multiplier,
487
+ ):
488
+ """The only thing on GPU time: the two reference encoders, the packed-sequence denoise loop, the decoders, and
489
+ the RCAS + FILM post chain. References cross as paths and are decoded here; only the generated outputs come
490
+ back. A `@spaces.GPU` argument crosses a process boundary by pickling, a 5 s 1344x768 reference video is
491
+ 370 MB of expanded frames, and the full `PipelineState` still holds the packed latents and the rotary grid on
492
+ the card.
493
  """
494
  import torch
495
+
496
+ global FILM
497
+
498
  if PLACEMENT == "lazy":
499
  PIPE.to("cuda")
500
  elif PLACEMENT == "pack":
501
  PIPE.vae.to("cuda")
502
  PIPE.audio_vae.to("cuda")
503
+
504
+ custom_schedule = schedule != "native"
505
+ # Any custom schedule hands `set_timesteps` a finished `steps + 1` sigma grid, so it runs `steps` forwards.
506
+ # The native grid counts its terminal zero as one of `num_inference_steps`, so it needs one more to match.
507
+ requested_steps = int(steps) if custom_schedule else int(steps) + 1
508
+
509
+ with pk.use_schedule(PIPE, int(steps), schedule, video_shift, audio_shift, sampler_name=sampler, seed=int(seed)):
510
+ state = PIPE(
511
+ prompt_embeds=prompt_embeds.to("cuda"),
512
+ text_token_tags=text_token_tags,
513
+ references=build_references(references),
514
+ height=height,
515
+ width=width,
516
+ num_frames=num_frames,
517
+ num_inference_steps=requested_steps,
518
+ generator=torch.Generator("cpu").manual_seed(int(seed)),
519
+ )
520
+
521
+ video = state.get("videos")[0] # (frames, 3, H, W), float in [0, 1], on the card
522
+ audio = state.get("audio")[0].cpu()
523
+ sampling_rate = state.get("sampling_rate")
524
+ del state
525
+ # The post chain runs on the allocator the denoise loop just left fragmented, and RCAS and FILM both want a
526
+ # few contiguous gigabytes.
527
+ torch.cuda.empty_cache()
528
+
529
+ video = pk.rcas(video, float(sharpen))
530
+ multiplier = max(1, int(multiplier))
531
+ if multiplier > 1:
532
+ if FILM is None:
533
+ multiplier = 1
534
+ else:
535
+ FILM = FILM.to("cuda")
536
+ video = pk.interpolate(FILM, video, multiplier)
537
+ fps = FPS * multiplier
538
+
539
+ return video, audio, sampling_rate, fps, multiplier
540
 
541
 
542
  def generate(
 
558
  steps=28,
559
  seed=42,
560
  upsample=False,
561
+ sampler=DEFAULT_SAMPLER,
562
+ schedule=DEFAULT_SCHEDULE,
563
+ video_shift=DEFAULT_VIDEO_SHIFT,
564
+ audio_shift=DEFAULT_AUDIO_SHIFT,
565
+ sharpen=DEFAULT_SHARPEN,
566
+ interpolation=DEFAULT_INTERPOLATION,
567
  progress=gr.Progress(track_tqdm=True),
568
  ):
569
  """One request."""
 
582
  # soundtrack. The conditioner resolves it either way and this Space pins whatever comes back.
583
  derivable = len(audio_bearing(references)) == 1
584
  requested = 0 if (match and derivable) else snap_frames(duration)
585
+ schedule_key = SCHEDULES.get(schedule, "linear_quadratic")
586
+ sampler_key = SAMPLERS.get(sampler, "euler")
587
+ multiplier = INTERPOLATION.get(interpolation, 2)
588
 
589
  progress(0.0, desc="Upsampling the prompt ..." if upsample else "Reading the prompt and references ...")
590
  conditioned = time.time()
 
608
 
609
  progress(0.1, desc=f"Generating {num_frames / FPS:.1f} s at {width}x{height} ...")
610
  started = time.time()
611
+ video, audio, sampling_rate, fps, multiplier = _generate(
612
+ prompt_embeds, text_token_tags, references, height, width, num_frames, steps, seed,
613
+ sampler_key, schedule_key, float(video_shift), float(audio_shift), float(sharpen), multiplier,
614
  )
615
  generate_seconds = time.time() - started
616
 
617
  directory = os.path.join(tempfile.gettempdir(), "h3-outputs")
618
  os.makedirs(directory, exist_ok=True)
619
  path = os.path.join(directory, f"h3-ref2va-{int(time.time() * 1000)}.mp4")
620
+ encode_video(video, fps=fps, output_path=path, audio=audio, audio_sample_rate=sampling_rate)
621
 
622
  print(
623
  f"[ref2va] {[kind for kind, _ in references]} · `{width}x{height}`, {num_frames} frames "
624
+ f"({num_frames / FPS:.3f} s), {int(steps)} steps of `{schedule_key}` · sampler `{sampler_key}` · "
625
+ f"shift {float(video_shift):.1f}/{float(audio_shift):.1f} · conditioner {condition_seconds:.0f}s "
626
  f"({plan['num_text_tokens']} tokens{', upsampled' if refined else ''}) · "
627
  f"denoise + decode {generate_seconds:.0f}s "
628
  f"({generate_seconds / int(steps):.1f} s/step) · seed {int(seed)}",
 
710
  audio_shift = gr.Slider(
711
  label="Audio shift", minimum=0.5, maximum=20.0, step=0.5, value=DEFAULT_AUDIO_SHIFT
712
  )
713
+ sharpen = gr.Slider(
714
+ label="RCAS sharpening",
715
+ minimum=0.0,
716
+ maximum=1.0,
717
+ step=0.05,
718
+ value=DEFAULT_SHARPEN,
719
+ info="FidelityFX Robust Contrast Adaptive Sharpening. PlagueKind: 0.3 is very natural.",
720
+ )
721
+ interpolation = gr.Dropdown(
722
+ label="FILM frame interpolation",
723
+ choices=list(INTERPOLATION),
724
+ value=DEFAULT_INTERPOLATION,
725
+ info="MiniMax-H3 generates 24 fps; FILM synthesizes the frames in between.",
726
+ )
727
  seed = gr.Number(label="Seed", value=42, precision=0)
728
 
729
  with gr.Column():
 
755
  video.change(auto_canvas, video, canvas, show_progress="hidden", api_name=False)
756
 
757
  request = [
758
+ prompt, images[0], audio, video, canvas, *images[1:], match, duration, steps, seed, upsample,
759
+ sampler, schedule, video_shift, audio_shift, sharpen, interpolation,
760
  ]
761
  run.click(generate, request, [result, upsampled, upsampled_panel], api_name="generate")
762