| """ZeroGPU reservation estimator, CPU preflight, and user-facing preview.""" |
| from __future__ import annotations |
|
|
| from math import ceil, isfinite |
|
|
| import gradio as gr |
|
|
| from space_config import FRAME_RATE, STANDARD_MAX_SECONDS |
| from .app_config import ( |
| EFFECTIVE_AUTO_DURATION_MAX_SECONDS, |
| IS_FULL_SFT_PROFILE, |
| ) |
| from .conditioning import mode_from_condition_images |
| from .ui_controls import frames_from_seconds, parse_resolution_value |
|
|
| |
| DURATION_ESTIMATOR_REVISION = "p34r0-v2" |
| FULL_SFT_DURATION_25F_FLOOR_SECONDS = 90 |
| FULL_SFT_DURATION_LONG_FLOOR_SECONDS = 110 |
| FULL_SFT_DURATION_SHORT_FIXED_SECONDS = 24.0 |
| FULL_SFT_DURATION_SHORT_SECONDS_PER_VIDEO_SECOND = 2.5 |
| FULL_SFT_DURATION_LONG_FIXED_SECONDS = 16.0 |
| FULL_SFT_DURATION_LONG_SECONDS_PER_VIDEO_SECOND = 10.2 |
| FULL_SFT_DURATION_LONG_GUARD_RATIO = 1.10 |
| FULL_SFT_DURATION_LONG_GUARD_SECONDS = 5.0 |
| |
| |
| |
| |
| |
| FULL_SFT_DURATION_LONG_DIFFUSION_DECODER_FIXED_SECONDS = 6.0 |
| FULL_SFT_DURATION_LONG_DIFFUSION_DECODER_SECONDS_PER_VIDEO_SECOND = 2.0 / 3.0 |
| DURATION_LORA_ALLOWANCE_SECONDS = 8.0 |
| DURATION_CONDITION_ALLOWANCE_SECONDS = { |
| "T2V": 0.0, "I2V": 2.0, "FLF2V": 4.0, "KEYFRAME": 4.0, "KEYFRAME_FLF2V": 6.0, |
| } |
| DISTILLED_DURATION_SHORT_FIXED_SECONDS = 8.0 |
| DISTILLED_DURATION_SHORT_SECONDS_PER_VIDEO_SECOND = 1.2 |
| DISTILLED_DURATION_LONG_FIXED_SECONDS = 18.0 |
| DISTILLED_DURATION_LONG_SECONDS_PER_VIDEO_SECOND = 1.8 |
|
|
|
|
| class DurationEstimateError(ValueError): |
| """A user/config input prevented a safe ZeroGPU reservation estimate.""" |
|
|
|
|
| def estimate_zerogpu_duration_seconds( |
| start_image_path, middle_image_path, end_image_path, duration_seconds, resolution_key, selected_loras, |
| use_diffusion_decoder, use_auto_duration, |
| ) -> int: |
| """Strict estimator shared by UI, CPU preflight, and the ZeroGPU scheduler callable.""" |
| try: |
| width, height = parse_resolution_value(resolution_key) |
| reservation_seconds = ( |
| EFFECTIVE_AUTO_DURATION_MAX_SECONDS if bool(use_auto_duration) else float(duration_seconds) |
| ) |
| except Exception as exc: |
| raise DurationEstimateError(f"invalid duration/resolution input: {exc}") from exc |
| if not isfinite(reservation_seconds) or reservation_seconds <= 0: |
| raise DurationEstimateError(f"duration must be a finite positive value, got {reservation_seconds!r}") |
| frames = frames_from_seconds(reservation_seconds) |
| realized_seconds = (frames - 1) / FRAME_RATE |
| area_ratio = max(1.0, (width * height) / float(512 * 512)) |
| lora_count = len(selected_loras or []) |
| mode = mode_from_condition_images(start_image_path, middle_image_path, end_image_path) |
| if mode == "INVALID_CONDITION_WITHOUT_START": |
| raise DurationEstimateError("Middle/End frame requires a Start frame") |
| if middle_image_path and bool(use_auto_duration): |
| raise DurationEstimateError("Middle keyframe requires Manual Duration so its midpoint is known before Stage 1") |
| condition_overhead = DURATION_CONDITION_ALLOWANCE_SECONDS.get(mode) |
| if condition_overhead is None: |
| raise DurationEstimateError(f"unsupported conditioning mode for duration estimate: {mode}") |
| lora_overhead = DURATION_LORA_ALLOWANCE_SECONDS * lora_count |
| |
| |
| |
| distilled_decoder_overhead = (6.0 + realized_seconds) if bool(use_diffusion_decoder) else 0.0 |
| full_sft_decoder_increment = ( |
| FULL_SFT_DURATION_LONG_DIFFUSION_DECODER_FIXED_SECONDS |
| + (FULL_SFT_DURATION_LONG_DIFFUSION_DECODER_SECONDS_PER_VIDEO_SECOND * realized_seconds) |
| if bool(use_diffusion_decoder) else 0.0 |
| ) |
| if IS_FULL_SFT_PROFILE: |
| short_estimate = ( |
| FULL_SFT_DURATION_SHORT_FIXED_SECONDS |
| + (FULL_SFT_DURATION_SHORT_SECONDS_PER_VIDEO_SECOND * realized_seconds * area_ratio) |
| + condition_overhead + lora_overhead + full_sft_decoder_increment |
| ) |
| short_requested = int(ceil(short_estimate)) |
| if frames <= 25: |
| return int(max(FULL_SFT_DURATION_25F_FLOOR_SECONDS, short_requested)) |
| long_center = ( |
| FULL_SFT_DURATION_LONG_FIXED_SECONDS |
| + (FULL_SFT_DURATION_LONG_SECONDS_PER_VIDEO_SECOND * realized_seconds * area_ratio) |
| + condition_overhead + lora_overhead + full_sft_decoder_increment |
| ) |
| long_requested = int(ceil( |
| (FULL_SFT_DURATION_LONG_GUARD_RATIO * long_center) + FULL_SFT_DURATION_LONG_GUARD_SECONDS |
| )) |
| return int(max(FULL_SFT_DURATION_LONG_FLOOR_SECONDS, short_requested, long_requested)) |
| if realized_seconds > STANDARD_MAX_SECONDS: |
| estimate = ( |
| DISTILLED_DURATION_LONG_FIXED_SECONDS |
| + (DISTILLED_DURATION_LONG_SECONDS_PER_VIDEO_SECOND * realized_seconds * area_ratio) |
| + condition_overhead + lora_overhead + distilled_decoder_overhead |
| ) |
| else: |
| estimate = ( |
| DISTILLED_DURATION_SHORT_FIXED_SECONDS |
| + (DISTILLED_DURATION_SHORT_SECONDS_PER_VIDEO_SECOND * realized_seconds * area_ratio) |
| + condition_overhead + lora_overhead + distilled_decoder_overhead |
| ) |
| return int(max(10, ceil(estimate))) |
|
|
|
|
| def generation_duration(prompt, start_image_path, middle_image_path, end_image_path, duration_seconds, experimental_long, resolution_key, seed, randomize_seed, |
| selected_loras, lora_strength, custom_loras, prepared_loras, use_diffusion_decoder, use_auto_duration, session_id, history, *args) -> int: |
| """ZeroGPU scheduler callable. Strict by design; CPU preflight runs first in product UI.""" |
| del prompt, experimental_long, seed, randomize_seed, lora_strength, custom_loras, prepared_loras, session_id, history, args |
| return estimate_zerogpu_duration_seconds( |
| start_image_path, middle_image_path, end_image_path, duration_seconds, resolution_key, selected_loras, |
| use_diffusion_decoder, use_auto_duration, |
| ) |
|
|
|
|
| def generation_duration_preflight( |
| start_image_path, middle_image_path, end_image_path, duration_seconds, resolution_key, selected_loras, |
| use_diffusion_decoder, use_auto_duration, |
| ): |
| try: |
| requested = estimate_zerogpu_duration_seconds( |
| start_image_path, middle_image_path, end_image_path, duration_seconds, resolution_key, selected_loras, |
| use_diffusion_decoder, use_auto_duration, |
| ) |
| except DurationEstimateError as exc: |
| raise gr.Error( |
| "ZeroGPU duration could not be estimated safely. GPU quota was not requested. " |
| f"Check Duration/Resolution/input mode and try again. Details: {exc}" |
| ) from exc |
| return f"Preflight ready · ZeroGPU reservation **{requested}s**. GPU request starts next." |
|
|
|
|
| def zerogpu_duration_panel(start_image_path, middle_image_path, end_image_path, duration_seconds, experimental_long, resolution_key, selected_loras, use_diffusion_decoder, use_auto_duration): |
| try: |
| width, height = parse_resolution_value(resolution_key) |
| except Exception: |
| return "⚠️ Resolution must be entered as WIDTH × HEIGHT with both dimensions multiples of 64." |
| reservation_seconds = EFFECTIVE_AUTO_DURATION_MAX_SECONDS if bool(use_auto_duration) else float(duration_seconds) |
| frames = frames_from_seconds(reservation_seconds) |
| realized_seconds = (frames - 1) / FRAME_RATE |
| try: |
| requested = estimate_zerogpu_duration_seconds( |
| start_image_path, middle_image_path, end_image_path, duration_seconds, resolution_key, selected_loras, |
| use_diffusion_decoder, use_auto_duration, |
| ) |
| except DurationEstimateError as exc: |
| return ( |
| "⚠️ **ZeroGPU duration estimate unavailable.** " |
| f"GPU generation will stop in CPU preflight instead of requesting a fallback duration. Details: `{exc}`" |
| ) |
| mode = mode_from_condition_images(start_image_path, middle_image_path, end_image_path) |
| lora_count = len(selected_loras or []) |
| mode_note = ( |
| "⚠️ Middle/End frame requires a Start frame; generation will be rejected until a Start frame is supplied." |
| if mode == "INVALID_CONDITION_WITHOUT_START" else f"Input mode: **{mode}**." |
| ) |
| if requested <= 60: |
| quota_note = "tight/ordinary reservation range for this app" |
| elif requested <= 120: |
| quota_note = "heavy reservation — quota impact is significant" |
| elif requested <= 300: |
| quota_note = "very heavy reservation — may consume a large share of a visitor's daily quota" |
| else: |
| quota_note = "aggressive reservation — visitor tier caps or remaining quota may reject it before execution" |
| full_sft_probe_note = "" |
| if IS_FULL_SFT_PROFILE and frames > 25: |
| full_sft_probe_note = ( |
| " \n**Full/SFT >25f reservation model:** P27/P28 Stage-1 scaling + P28R4 cold Conv-VAE tail define the base model; P33's 721f Diffusion Decoder witness now calibrates decoder-vs-Conv incremental cost. " |
| "The 110s value is only the minimum floor; long requests scale above it. Runtime evidence is hardware-specific; unverified combinations remain available rather than being product-blocked." |
| ) |
| long_note = "" |
| if realized_seconds > STANDARD_MAX_SECONDS: |
| long_note = ( |
| " \n⚠️ **Experimental video length.** The 30s / 721f endpoint is live-passed for distilled T2V and I2V at 512×512; " |
| "long durations remain opt-in and quota/VRAM cost rises with length." |
| ) |
| flf_note = "" |
| if mode == "FLF2V": |
| flf_note = ( |
| " \n⚠️ **FLF2V remains experimental.** 512×512/25f current-NF4 has a returned ZeroGPU 48GB PASS; " |
| "longer durations and other FLF2V resolutions remain unvalidated." |
| ) |
| keyframe_note = "" |
| if middle_image_path: |
| keyframe_note = ( |
| " \n⚠️ **Middle keyframe is experimental.** It is fixed at the nearest latent-grid midpoint of Manual Duration; " |
| "Auto Duration is intentionally rejected before GPU reservation while a Middle image is present." |
| ) |
| vram_note = "" |
| if width == 512 and height == 512 and mode == "T2V": |
| predicted_gib = 26.5189 + (0.0254508 * frames) |
| vram_note = ( |
| f" \nRough 512×512 T2V peak-allocation extrapolation: **{predicted_gib:.1f} GiB** " |
| "(measured linear trend through 241f; not a guarantee)." |
| ) |
| duration_contract = ( |
| f"Auto Duration · reservation basis {EFFECTIVE_AUTO_DURATION_MAX_SECONDS:.1f}s cap" |
| if bool(use_auto_duration) else f"Manual · {frames}f · {realized_seconds:.2f}s video" |
| ) |
| auto_note = ( |
| " \nAuto Duration is predicted only after GPU allocation; ZeroGPU therefore reserves against the configured max cap, " |
| "not the eventual model-predicted length. The actual output is snapped to the upstream 8k+1 grid and may be shorter than the cap." |
| if bool(use_auto_duration) else "" |
| ) |
| return ( |
| f"**Estimated ZeroGPU request: {requested} seconds** — {quota_note}. \n" |
| f"{mode_note} Contract: **{width}×{height} · {duration_contract} · {lora_count} LoRA(s) · decoder {'Diffusion' if use_diffusion_decoder else 'Conv VAE'}**. \n" |
| "This is the requested GPU reservation/quota budget, not the video duration or a guaranteed wall-clock runtime." |
| f"{auto_note}{full_sft_probe_note}{long_note}{flf_note}{keyframe_note}{vram_note}" |
| ) |
|
|