Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 709 |
total_steps=total_steps, stage_from=stage_from,
|
| 710 |
):
|
| 711 |
-
with use_dpmpp_2s_ancestral(
|
| 712 |
-
with use_dpmpp_sde_gpu(
|
| 713 |
-
with use_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 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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
|