dagloop5 commited on
Commit
60b324f
·
verified ·
1 Parent(s): 8636286

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -24
app.py CHANGED
@@ -172,6 +172,9 @@ DEFAULT_STEPS = 15
172
  DEFAULT_TARGET_STEPS = 25
173
  # Chunked Generation: an arbitrary, adjustable starting point for the "Chunk stop (s)" field.
174
  DEFAULT_CHUNK_STOP = 10.0
 
 
 
175
 
176
 
177
  def snap_frames(seconds: float) -> int:
@@ -370,6 +373,8 @@ def _convert_diffusion_model_lora(raw: dict, base_shapes: dict, swap_fc1: bool)
370
  return out, network_alphas
371
 
372
  PIPE = None
 
 
373
  FILM = None
374
  FILM_ERROR: str | None = None
375
  LOAD_ERROR: str | None = None
@@ -385,9 +390,10 @@ def status() -> str:
385
  return f"Loading `{MODEL_REPO}` (transformer + VAEs, 77.3 GB). Watch the Space logs."
386
 
387
  film = "FILM **ready**" if FILM is not None else f"FILM **off** ({FILM_ERROR})"
 
388
  return (
389
  f"Ready · transformer + VAEs **bfloat16, unquantized** · placement `{PLACEMENT}` · attention "
390
- f"`{ATTENTION}` · {film} · {LORA_STATUS or 'no LoRA'} · loaded in {LOADED_IN:.0f}s · "
391
  f"conditioner `{CONDITIONER_SPACE}`"
392
  )
393
 
@@ -399,7 +405,7 @@ def load_models() -> str | None:
399
  touched. Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio
400
  VAE decodes the soundtrack roughly 20 dB too quiet.
401
  """
402
- global PIPE, FILM, FILM_ERROR, LOAD_ERROR, LOADED_IN, LORA_STATUS
403
 
404
  if PIPE is not None or LOAD_ERROR is not None:
405
  return LOAD_ERROR
@@ -513,6 +519,28 @@ def load_models() -> str | None:
513
  # ~10 GB of fp32 VAEs move on the first GPU call instead.
514
  pipe.transformer.to("cuda")
515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  PIPE = pipe
517
  LOADED_IN = time.time() - started
518
  print(f"[gen] ready in {LOADED_IN:.0f}s", flush=True)
@@ -610,6 +638,12 @@ def get_duration(
610
  if maximize_gpu:
611
  return MAXIMIZE_GPU_DURATION
612
 
 
 
 
 
 
 
613
  height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
614
  multiplier = max(1, int(multiplier))
615
  latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
@@ -626,8 +660,7 @@ def get_duration(
626
  film = (num_frames - 1) * (multiplier - 1) * _FILM_PER_FRAME * pixel_ratio
627
  post = _POST_BASE + film + out_frames * _MUX_PER_FRAME * pixel_ratio
628
 
629
- return max(60, int((denoise + decode + post) * _MARGIN) + _PLACEMENT_ALLOWANCE)
630
-
631
 
632
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
633
  def _generate(
@@ -652,6 +685,8 @@ def _generate(
652
  stage_from,
653
  resume_video_latents,
654
  resume_audio_latents,
 
 
655
  ):
656
  """The only thing on GPU time: the denoise loop, the two decoders and the workflow's post chain.
657
  The mp4 is muxed here rather than in the caller: a `@spaces.GPU` return crosses a process boundary by pickling,
@@ -704,13 +739,17 @@ def _generate(
704
  # exact same bits, every time — offsetting by `stage_from` (0 on a fresh/single-stage call, so this changes
705
  # nothing there) means each stage actually draws a different stream.
706
  effective_seed = int(seed) + stage_from
 
 
 
 
707
  with pk.use_schedule(
708
- PIPE, steps, schedule, video_shift, audio_shift, sampler_name=sampler, seed=effective_seed,
709
  total_steps=total_steps, stage_from=stage_from,
710
  ):
711
- with use_dpmpp_2s_ancestral(PIPE, effective_seed, enabled=(sampler == "dpmpp_2s_ancestral")):
712
- with use_dpmpp_sde_gpu(PIPE, effective_seed, enabled=(sampler == "dpmpp_sde_gpu")):
713
- with use_seeds_2(PIPE, effective_seed, enabled=(sampler == "seeds_2")):
714
  # Staged Denoising: resuming hands the pipeline the previous stage's own latents instead of
715
  # letting `PrepareLatentsStep` draw fresh noise — both are declared-optional inputs on that
716
  # step precisely for this ("used instead of the draw"), so nothing else about the call
@@ -720,19 +759,36 @@ def _generate(
720
  if resume_video_latents is not None
721
  else {}
722
  )
723
- state = PIPE(
724
- prompt_embeds=prompt_embeds.to("cuda"),
725
- text_token_tags=text_token_tags,
726
- image=first_frame,
727
- last_image=last_frame,
728
- height=height,
729
- width=width,
730
- num_frames=num_frames,
731
- num_inference_steps=requested_steps,
732
- output_type="pt",
733
- generator=torch.Generator("cpu").manual_seed(int(seed)),
734
- **resume_kwargs,
735
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
736
 
737
  denoised = time.time() - started
738
 
@@ -810,6 +866,7 @@ def generate(
810
  chunk_start=0.0,
811
  chunk_stop=DEFAULT_CHUNK_STOP,
812
  chunk_state=None,
 
813
  progress=gr.Progress(track_tqdm=True),
814
  *,
815
  advance: bool = False,
@@ -864,7 +921,16 @@ def generate(
864
  if chunk_advance and chunk_state is None:
865
  raise gr.Error("Press Generate with Chunked Generation enabled first, to start a chunked sequence.")
866
 
867
- chunk_first_frame = _last_frame_path(chunk_state["paths"][-1]) if chunk_advance else None
 
 
 
 
 
 
 
 
 
868
  effective_first_frame = chunk_first_frame if chunk_advance else first_frame
869
  effective_last_frame = None if chunk_enabled else last_frame
870
 
@@ -945,6 +1011,8 @@ def generate(
945
  steps_done if advance else 0,
946
  stage_state["video_latents"] if advance else None,
947
  stage_state["audio_latents"] if advance else None,
 
 
948
  )
949
  # The same call `spaces` will book the worker with, so the report can show the fit against the measurement.
950
  booked_seconds = get_duration(*call)
@@ -1102,6 +1170,43 @@ def _last_frame_path(video_path: str) -> str | None:
1102
  return path
1103
 
1104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
  def _concat_chunks(paths: list[str]) -> str:
1106
  """The chunks so far, concatenated with a stream copy (no re-encode) — cheap regardless of how many segments
1107
  are in the list, so redoing the whole concat fresh on every press is simpler and more robust than trying to
@@ -1387,6 +1492,19 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
1387
  chunk_enabled = gr.Checkbox(label="Enable chunked generation", value=False)
1388
  chunk_start = gr.Number(label="Chunk start (s)", value=0.0, visible=False)
1389
  chunk_stop = gr.Number(label="Chunk stop (s)", value=DEFAULT_CHUNK_STOP, visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
1390
  continue_btn = gr.Button("Continue", variant="secondary", visible=False)
1391
 
1392
  stage_state = gr.State(None)
@@ -1414,9 +1532,9 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
1414
  api_name=False,
1415
  )
1416
  chunk_enabled.change(
1417
- lambda enabled: tuple(gr.update(visible=enabled) for _ in range(3)),
1418
  chunk_enabled,
1419
- [chunk_start, chunk_stop, continue_btn],
1420
  api_name=False,
1421
  )
1422
 
@@ -1457,6 +1575,7 @@ with gr.Blocks(title="PlagueKind · MiniMax-H3") as demo:
1457
  chunk_start,
1458
  chunk_stop,
1459
  chunk_state,
 
1460
  ]
1461
 
1462
  # `functools.partial` binds `advance`/`chunk_advance` by keyword regardless of their position in `generate`'s
 
172
  DEFAULT_TARGET_STEPS = 25
173
  # Chunked Generation: an arbitrary, adjustable starting point for the "Chunk stop (s)" field.
174
  DEFAULT_CHUNK_STOP = 10.0
175
+ # Momentum: seconds of the previous chunk's tail carried into the next chunk's opening. 0 disables momentum
176
+ # entirely, falling back to the plain last-frame-as-keyframe carry.
177
+ DEFAULT_MOMENTUM = 2.0
178
 
179
 
180
  def snap_frames(seconds: float) -> int:
 
373
  return out, network_alphas
374
 
375
  PIPE = None
376
+ MOMENTUM_PIPE = None
377
+ MOMENTUM_ERROR: str | None = None
378
  FILM = None
379
  FILM_ERROR: str | None = None
380
  LOAD_ERROR: str | None = None
 
390
  return f"Loading `{MODEL_REPO}` (transformer + VAEs, 77.3 GB). Watch the Space logs."
391
 
392
  film = "FILM **ready**" if FILM is not None else f"FILM **off** ({FILM_ERROR})"
393
+ momentum = "momentum **ready**" if MOMENTUM_PIPE is not None else f"momentum **off** ({MOMENTUM_ERROR})"
394
  return (
395
  f"Ready · transformer + VAEs **bfloat16, unquantized** · placement `{PLACEMENT}` · attention "
396
+ f"`{ATTENTION}` · {film} · {momentum} · {LORA_STATUS or 'no LoRA'} · loaded in {LOADED_IN:.0f}s · "
397
  f"conditioner `{CONDITIONER_SPACE}`"
398
  )
399
 
 
405
  touched. Both autoencoders carry `_keep_in_fp32_modules` over every module and stay float32: a bfloat16 audio
406
  VAE decodes the soundtrack roughly 20 dB too quiet.
407
  """
408
+ global PIPE, FILM, FILM_ERROR, LOAD_ERROR, LOADED_IN, LORA_STATUS, MOMENTUM_PIPE, MOMENTUM_ERROR
409
 
410
  if PIPE is not None or LOAD_ERROR is not None:
411
  return LOAD_ERROR
 
519
  # ~10 GB of fp32 VAEs move on the first GPU call instead.
520
  pipe.transformer.to("cuda")
521
 
522
+ # Chunked Generation's momentum feature: a second, keyframe-free denoise graph over the *same* resident
523
+ # weights — `update_components` links it to `pipe`'s own `transformer`/`vae`/`audio_vae`/schedulers, so
524
+ # nothing here is loaded a second time. Soft-fail like FILM below: an experimental, newly-added block
525
+ # (`h3_momentum.py`, untested end to end) failing here shouldn't take the whole Space down with it.
526
+ try:
527
+ from h3_momentum import MiniMaxH3MomentumGeneratorBlocks
528
+
529
+ momentum_blocks = MiniMaxH3MomentumGeneratorBlocks()
530
+ momentum_pipe = momentum_blocks.init_pipeline(MODEL_REPO, components_manager=manager, collection="h3")
531
+ momentum_pipe.update_components(
532
+ transformer=pipe.transformer,
533
+ vae=pipe.vae,
534
+ audio_vae=pipe.audio_vae,
535
+ scheduler=pipe.scheduler,
536
+ audio_scheduler=pipe.audio_scheduler,
537
+ )
538
+ MOMENTUM_PIPE = momentum_pipe
539
+ print("[gen] momentum pipe ready", flush=True)
540
+ except Exception as error:
541
+ MOMENTUM_ERROR = f"{type(error).__name__}: {error}"
542
+ print(f"[gen] momentum pipe unavailable ({MOMENTUM_ERROR}); momentum disabled", flush=True)
543
+
544
  PIPE = pipe
545
  LOADED_IN = time.time() - started
546
  print(f"[gen] ready in {LOADED_IN:.0f}s", flush=True)
 
638
  if maximize_gpu:
639
  return MAXIMIZE_GPU_DURATION
640
 
641
+ # Momentum: `given_video` is the second-to-last item of `*a`, matching its fixed position at the end of the
642
+ # `call` tuple in `generate()`. A flat, unvalidated allowance for the extra VAE encode — worth checking
643
+ # against a real measurement once this is testable, the same as every other constant in this function was.
644
+ given_video = a[-2] if len(a) >= 2 else None
645
+ momentum_allowance = 5 if given_video is not None else 0
646
+
647
  height, width, num_frames, steps = int(height), int(width), int(num_frames), int(steps)
648
  multiplier = max(1, int(multiplier))
649
  latent_frames = (num_frames - LATENTS_PER_CHUNK) // FRAMES_PER_CHUNK * LATENTS_PER_CHUNK + 2
 
660
  film = (num_frames - 1) * (multiplier - 1) * _FILM_PER_FRAME * pixel_ratio
661
  post = _POST_BASE + film + out_frames * _MUX_PER_FRAME * pixel_ratio
662
 
663
+ return max(60, int((denoise + decode + post + momentum_allowance) * _MARGIN) + _PLACEMENT_ALLOWANCE)
 
664
 
665
  @spaces.GPU(duration=get_duration, size=GPU_SIZE)
666
  def _generate(
 
685
  stage_from,
686
  resume_video_latents,
687
  resume_audio_latents,
688
+ given_video,
689
+ video_condition_mode,
690
  ):
691
  """The only thing on GPU time: the denoise loop, the two decoders and the workflow's post chain.
692
  The mp4 is muxed here rather than in the caller: a `@spaces.GPU` return crosses a process boundary by pickling,
 
739
  # exact same bits, every time — offsetting by `stage_from` (0 on a fresh/single-stage call, so this changes
740
  # nothing there) means each stage actually draws a different stream.
741
  effective_seed = int(seed) + stage_from
742
+ # Momentum: a genuinely different, keyframe-free denoise graph, sharing every weight with `PIPE` (see
743
+ # `load_models()`) — `pk.use_schedule`/the sampler context managers are already generic over whichever pipe
744
+ # object they're handed, since both read and write the same shared `scheduler`/`audio_scheduler`/`transformer`.
745
+ active_pipe = MOMENTUM_PIPE if given_video is not None else PIPE
746
  with pk.use_schedule(
747
+ active_pipe, steps, schedule, video_shift, audio_shift, sampler_name=sampler, seed=effective_seed,
748
  total_steps=total_steps, stage_from=stage_from,
749
  ):
750
+ with use_dpmpp_2s_ancestral(active_pipe, effective_seed, enabled=(sampler == "dpmpp_2s_ancestral")):
751
+ with use_dpmpp_sde_gpu(active_pipe, effective_seed, enabled=(sampler == "dpmpp_sde_gpu")):
752
+ with use_seeds_2(active_pipe, effective_seed, enabled=(sampler == "seeds_2")):
753
  # Staged Denoising: resuming hands the pipeline the previous stage's own latents instead of
754
  # letting `PrepareLatentsStep` draw fresh noise — both are declared-optional inputs on that
755
  # step precisely for this ("used instead of the draw"), so nothing else about the call
 
759
  if resume_video_latents is not None
760
  else {}
761
  )
762
+ if given_video is not None:
763
+ # Momentum: keyframe-free, so no `image`/`last_image` at all — the carried clip already
764
+ # determines the opening frames more directly than a keyframe could.
765
+ state = active_pipe(
766
+ prompt_embeds=prompt_embeds.to("cuda"),
767
+ text_token_tags=text_token_tags,
768
+ height=height,
769
+ width=width,
770
+ num_frames=num_frames,
771
+ num_inference_steps=requested_steps,
772
+ output_type="pt",
773
+ generator=torch.Generator("cpu").manual_seed(int(seed)),
774
+ given_video=given_video.to("cuda"),
775
+ video_condition_mode=video_condition_mode,
776
+ **resume_kwargs,
777
+ )
778
+ else:
779
+ state = active_pipe(
780
+ prompt_embeds=prompt_embeds.to("cuda"),
781
+ text_token_tags=text_token_tags,
782
+ image=first_frame,
783
+ last_image=last_frame,
784
+ height=height,
785
+ width=width,
786
+ num_frames=num_frames,
787
+ num_inference_steps=requested_steps,
788
+ output_type="pt",
789
+ generator=torch.Generator("cpu").manual_seed(int(seed)),
790
+ **resume_kwargs,
791
+ )
792
 
793
  denoised = time.time() - started
794
 
 
866
  chunk_start=0.0,
867
  chunk_stop=DEFAULT_CHUNK_STOP,
868
  chunk_state=None,
869
+ momentum=DEFAULT_MOMENTUM,
870
  progress=gr.Progress(track_tqdm=True),
871
  *,
872
  advance: bool = False,
 
921
  if chunk_advance and chunk_state is None:
922
  raise gr.Error("Press Generate with Chunked Generation enabled first, to start a chunked sequence.")
923
 
924
+ chunk_first_frame = None
925
+ given_video = None
926
+ video_condition_mode = "blended"
927
+ if chunk_advance:
928
+ if MOMENTUM_PIPE is not None and float(momentum) > 0:
929
+ given_video = _trailing_frames(chunk_state["paths"][-1], float(momentum))
930
+ if given_video is None:
931
+ # Momentum off, unavailable, or the extraction came back empty — fall back to the plain
932
+ # last-frame-as-keyframe carry rather than dropping continuity entirely.
933
+ chunk_first_frame = _last_frame_path(chunk_state["paths"][-1])
934
  effective_first_frame = chunk_first_frame if chunk_advance else first_frame
935
  effective_last_frame = None if chunk_enabled else last_frame
936
 
 
1011
  steps_done if advance else 0,
1012
  stage_state["video_latents"] if advance else None,
1013
  stage_state["audio_latents"] if advance else None,
1014
+ given_video,
1015
+ video_condition_mode,
1016
  )
1017
  # The same call `spaces` will book the worker with, so the report can show the fit against the measurement.
1018
  booked_seconds = get_duration(*call)
 
1170
  return path
1171
 
1172
 
1173
+ def _trailing_frames(video_path: str, seconds: float):
1174
+ """The last `seconds` of `video_path`'s pixel frames, at MiniMax-H3's own native 24 fps, as `(num_frames, 3,
1175
+ H, W)` float in `[0, 1]` — the format `MiniMaxH3MomentumConditionStep` expects for `given_video`. Strided
1176
+ back down to 24 fps if the saved chunk was FILM-interpolated to a multiple of it: feeding the video VAE
1177
+ frames at anything other than its own encoding rate would encode the motion at the wrong speed — a
1178
+ real correctness point, not a cosmetic one, since FILM's own multiplier is read fresh from the UI on every
1179
+ press. Runs on CPU; no GPU time.
1180
+ """
1181
+ if not video_path or seconds <= 0:
1182
+ return None
1183
+ import cv2
1184
+ import numpy as np
1185
+ import torch
1186
+
1187
+ cap = cv2.VideoCapture(video_path)
1188
+ if not cap.isOpened():
1189
+ return None
1190
+ actual_fps = cap.get(cv2.CAP_PROP_FPS) or FPS
1191
+ stride = max(1, round(actual_fps / FPS))
1192
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
1193
+ start_frame = max(0, total_frames - round(seconds * actual_fps))
1194
+
1195
+ cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
1196
+ frames = []
1197
+ for index in range(total_frames - start_frame):
1198
+ ok, frame = cap.read()
1199
+ if not ok:
1200
+ break
1201
+ if index % stride == 0:
1202
+ frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
1203
+ cap.release()
1204
+ if not frames:
1205
+ return None
1206
+ array = np.stack(frames).astype(np.float32) / 255.0
1207
+ return torch.from_numpy(array).permute(0, 3, 1, 2).contiguous()
1208
+
1209
+
1210
  def _concat_chunks(paths: list[str]) -> str:
1211
  """The chunks so far, concatenated with a stream copy (no re-encode) — cheap regardless of how many segments
1212
  are in the list, so redoing the whole concat fresh on every press is simpler and more robust than trying to
 
1492
  chunk_enabled = gr.Checkbox(label="Enable chunked generation", value=False)
1493
  chunk_start = gr.Number(label="Chunk start (s)", value=0.0, visible=False)
1494
  chunk_stop = gr.Number(label="Chunk stop (s)", value=DEFAULT_CHUNK_STOP, visible=False)
1495
+ momentum = gr.Slider(
1496
+ label="Momentum (s)",
1497
+ minimum=0.0,
1498
+ maximum=5.0,
1499
+ step=0.5,
1500
+ value=DEFAULT_MOMENTUM,
1501
+ visible=False,
1502
+ info=(
1503
+ "Seconds of the previous chunk's tail imposed on the next chunk's opening, for real "
1504
+ "motion continuity — 0 falls back to a plain last-frame keyframe. Untested past a "
1505
+ "couple of seconds."
1506
+ ),
1507
+ )
1508
  continue_btn = gr.Button("Continue", variant="secondary", visible=False)
1509
 
1510
  stage_state = gr.State(None)
 
1532
  api_name=False,
1533
  )
1534
  chunk_enabled.change(
1535
+ lambda enabled: tuple(gr.update(visible=enabled) for _ in range(4)),
1536
  chunk_enabled,
1537
+ [chunk_start, chunk_stop, momentum, continue_btn],
1538
  api_name=False,
1539
  )
1540
 
 
1575
  chunk_start,
1576
  chunk_stop,
1577
  chunk_state,
1578
+ momentum,
1579
  ]
1580
 
1581
  # `functools.partial` binds `advance`/`chunk_advance` by keyword regardless of their position in `generate`'s