| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from pathlib import Path |
|
|
| import gradio as gr |
|
|
| from shared.utils.audio_video import extract_audio_tracks |
| from shared.utils.utils import get_video_info_details |
| from shared.utils.video_decode import resolve_media_binary |
| from shared.utils.virtual_media import build_virtual_media_path |
|
|
| from . import common |
| from . import frame_planning as frames |
| from . import media_io as media |
| from . import output_paths |
| from . import process_metadata |
| from . import video_buffers as video |
|
|
|
|
| class ProcessInfoExit(Exception): |
| def __init__(self, message: str, *, output_path: str | None = None) -> None: |
| super().__init__(message) |
| self.message = message |
| self.output_path = output_path |
|
|
|
|
| @dataclass(frozen=True) |
| class PreparedRun: |
| verbose_level: int |
| start_frame: int |
| end_frame_exclusive: int |
| fps_float: float |
| total_source_frames: int |
| processing_fps: float |
| selected_audio_track: int | None |
| frame_plan_rules: frames.FramePlanRules |
| budget_resolution: str |
| chunk_frames: int |
| overlap_frames: int |
| full_plans: list[frames.ChunkPlan] |
| requested_unique_frames: int |
| requested_source_segment: str |
| output_path: str |
| resume_existing_output: bool |
| ffmpeg_path: str |
| ffprobe_path: str |
| output_container: str |
| merged_continuation_signatures: list[dict] |
|
|
|
|
| def get_mmgp_verbose_level() -> int: |
| try: |
| from mmgp import offload |
| return int(getattr(offload, "default_verboseLevel", 0) or 0) |
| except Exception: |
| return 0 |
|
|
|
|
| def prepare_run( |
| *, |
| plugin, |
| get_model_def, |
| process_name: str, |
| process_display_name: str, |
| source_path: str, |
| output_path: str, |
| output_resolution: str, |
| active_target_ratio: str, |
| continue_enabled: bool, |
| source_audio_track: str, |
| chunk_size_seconds: float, |
| sliding_window_overlap: int, |
| start_seconds: float, |
| end_seconds: float | None, |
| model_type: str, |
| process_is_hdr: bool, |
| uses_builtin_outpaint_ui: bool, |
| system_handler=None, |
| system_target_control: str = "", |
| ) -> PreparedRun: |
| verbose_level = get_mmgp_verbose_level() |
| try: |
| metadata = get_video_info_details(source_path) |
| except Exception as exc: |
| raise gr.Error(f"Unable to read source video metadata: {source_path}") from exc |
| start_frame, end_frame_exclusive, fps_float, total_source_frames = video.compute_selected_frame_range(metadata, start_seconds, end_seconds) |
| processing_fps = video.get_processing_fps(fps_float) |
| try: |
| audio_track_count = int(extract_audio_tracks(source_path, query_only=True)) |
| except Exception as exc: |
| raise gr.Error(f"Unable to inspect source audio tracks in: {source_path}") from exc |
| selected_audio_track = None |
| if len(source_audio_track) > 0: |
| if not source_audio_track.isdigit() or int(source_audio_track) <= 0: |
| raise gr.Error("Source Audio must be Auto or a whole track number.") |
| if audio_track_count <= 0: |
| raise gr.Error("Source video contains no audio track. Leave Source Audio empty.") |
| selected_audio_track = int(source_audio_track) |
| elif audio_track_count > 0: |
| selected_audio_track = 1 |
| if selected_audio_track is not None and (selected_audio_track <= 0 or selected_audio_track > audio_track_count): |
| raise gr.Error(f"Source Audio must be between 1 and {audio_track_count}.") |
| if system_handler is None: |
| frame_plan_rules = frames.get_frame_plan_rules(model_type, get_model_def) |
| budget_resolution = output_paths.choose_resolution(output_resolution) |
| output_resolution_token = output_resolution |
| else: |
| frame_plan_rules = frames.FramePlanRules(frame_step=int(getattr(system_handler, "frame_step", 1)), minimum_requested_frames=int(getattr(system_handler, "minimum_requested_frames", 1))) |
| budget_resolution = "" |
| output_resolution_token = system_handler.output_resolution_token(system_target_control) if hasattr(system_handler, "output_resolution_token") else output_resolution |
| try: |
| chunk_frames = frames.normalize_chunk_frames(chunk_size_seconds, processing_fps, frame_step=frame_plan_rules.frame_step, minimum_requested_frames=frame_plan_rules.minimum_requested_frames) |
| if system_handler is not None: |
| get_overlap_frames = getattr(system_handler, "get_overlap_frames", None) |
| overlap_frames = int(get_overlap_frames(chunk_frames)) if callable(get_overlap_frames) else int(getattr(system_handler, "overlap_frames", 0)) |
| else: |
| overlap_frames = frames.normalize_overlap_frames(sliding_window_overlap, frame_step=frame_plan_rules.frame_step) |
| except frames.FramePlanningError as exc: |
| raise gr.Error(str(exc)) from exc |
| if overlap_frames >= chunk_frames: |
| raise gr.Error(f"Sliding Window Overlap must stay below the computed chunk size ({chunk_frames} frame(s)).") |
| selected_unique_frames = end_frame_exclusive - start_frame |
| try: |
| full_plans = frames.build_chunk_plan( |
| start_frame, |
| end_frame_exclusive, |
| total_source_frames, |
| chunk_frames, |
| frame_step=frame_plan_rules.frame_step, |
| minimum_requested_frames=frame_plan_rules.minimum_requested_frames, |
| overlap_frames=overlap_frames, |
| ) |
| except frames.FramePlanningError as exc: |
| raise gr.Error(str(exc)) from exc |
| requested_unique_frames = frames.count_planned_unique_frames(full_plans) |
| requested_source_segment = build_virtual_media_path(source_path, start_frame=start_frame, end_frame=start_frame + requested_unique_frames - 1, audio_track_no=selected_audio_track) |
| default_output_container = media.normalize_container_name(plugin.server_config.get("video_container", "mp4")) |
| requested_output_path = str(output_paths.build_requested_output_path(source_path, output_path, process_display_name, active_target_ratio, output_resolution_token, start_seconds, end_seconds, has_outpaint=uses_builtin_outpaint_ui, default_container=default_output_container)) |
| if continue_enabled: |
| existing_identity = process_metadata.read_output_identity(requested_output_path) |
| identity_mismatch_message = process_metadata.get_output_identity_mismatch_message(requested_output_path, process_name=process_display_name, source_path=source_path, source_segment=requested_source_segment) |
| if identity_mismatch_message is not None: |
| system_supports_continue_cache = system_handler is not None and (not callable(getattr(system_handler, "supports_continue_cache_for_target", None)) or system_handler.supports_continue_cache_for_target(system_target_control)) |
| can_resume_from_system_state = existing_identity is None and system_supports_continue_cache and callable(getattr(system_handler, "can_resume_without_output_metadata", None)) and system_handler.can_resume_without_output_metadata(requested_output_path) |
| if not can_resume_from_system_state: |
| raise ProcessInfoExit(identity_mismatch_message, output_path=requested_output_path) |
| common.plugin_info(f"Existing output has no readable WanGP metadata, but the system process can continue from its cache or decoded output tail: {requested_output_path}") |
| resolved_output_path, resume_existing_output = output_paths.resolve_output_path(source_path, output_path, process_display_name, active_target_ratio, output_resolution_token, start_seconds, end_seconds, continue_enabled, has_outpaint=uses_builtin_outpaint_ui, default_container=default_output_container, notify=common.plugin_info) |
| try: |
| Path(resolved_output_path).parent.mkdir(parents=True, exist_ok=True) |
| except OSError as exc: |
| raise gr.Error(f"Unable to create the output folder for: {resolved_output_path}") from exc |
| dropped_tail_frames = selected_unique_frames - requested_unique_frames |
| if dropped_tail_frames > 0: |
| common.plugin_info(f"Dropping the last {dropped_tail_frames} source frame(s) so the selected range fits the current model chunk shape.") |
| ffmpeg_path = resolve_media_binary("ffmpeg") |
| if ffmpeg_path is None: |
| raise gr.Error("ffmpeg binary not found.") |
| ffprobe_path = resolve_media_binary("ffprobe") |
| if ffprobe_path is None: |
| raise gr.Error("ffprobe binary not found.") |
| output_container = media.normalize_container_name(Path(resolved_output_path).suffix.lstrip(".") or plugin.server_config.get("video_container", "mp4")) |
| output_video_codec = "libx265_8" if process_is_hdr else plugin.server_config.get("video_output_codec", "libx264_8") |
| media.validate_output_codec_container(output_video_codec, output_container, output_path=resolved_output_path) |
| if selected_audio_track is not None: |
| media.validate_audio_copy_container(ffprobe_path, source_path, output_container, selected_audio_track) |
| return PreparedRun( |
| verbose_level=verbose_level, |
| start_frame=start_frame, |
| end_frame_exclusive=end_frame_exclusive, |
| fps_float=fps_float, |
| total_source_frames=total_source_frames, |
| processing_fps=processing_fps, |
| selected_audio_track=selected_audio_track, |
| frame_plan_rules=frame_plan_rules, |
| budget_resolution=budget_resolution, |
| chunk_frames=chunk_frames, |
| overlap_frames=overlap_frames, |
| full_plans=full_plans, |
| requested_unique_frames=requested_unique_frames, |
| requested_source_segment=requested_source_segment, |
| output_path=resolved_output_path, |
| resume_existing_output=resume_existing_output, |
| ffmpeg_path=ffmpeg_path, |
| ffprobe_path=ffprobe_path, |
| output_container=output_container, |
| merged_continuation_signatures=process_metadata.read_merged_continuation_signatures(resolved_output_path), |
| ) |
|
|