Spaces:
Runtime error
Runtime error
Download mcp_tools/common.py from Samuelsrmendozs/MiniMax-H3-e: direct link, hf CLI and curl.
- Browser
- Download file 25.4 kB
-
https://huggingface.co/spaces/Samuelsrmendozs/MiniMax-H3-e/resolve/main/mcp_tools/common.py
- Command line
-
hf download hf://spaces/Samuelsrmendozs/MiniMax-H3-e/mcp_tools/common.py
-
curl -L -o common.py https://huggingface.co/spaces/Samuelsrmendozs/MiniMax-H3-e/resolve/main/mcp_tools/common.py
25.4 kB
| """ | |
| MCP Common Utilities & Data Structures | |
| Contains image parsing utilities, task definitions, aspect ratio presets, and async task database for MiniMax H3 Video Generation. | |
| """ | |
| import os | |
| import time | |
| import uuid | |
| import urllib.parse | |
| import urllib.request | |
| import urllib.error | |
| import base64 | |
| import io | |
| import yaml | |
| from typing import Dict, Any | |
| from PIL import Image | |
| _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| _YAML_DIR = os.path.join(_PROJECT_ROOT, "yaml") | |
| _MAX_IMAGE_DOWNLOAD_BYTES = 50 * 1024 * 1024 # 50 MB | |
| _IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds | |
| _MAX_VIDEO_DOWNLOAD_BYTES = 200 * 1024 * 1024 # 200 MB | |
| _VIDEO_DOWNLOAD_TIMEOUT = 60 # seconds | |
| _MAX_AUDIO_DOWNLOAD_BYTES = 100 * 1024 * 1024 # 100 MB | |
| _AUDIO_DOWNLOAD_TIMEOUT = 60 # seconds | |
| _ALLOWED_IMAGE_CONTENT_TYPES = frozenset([ | |
| "image/png", "image/jpeg", "image/jpg", "image/gif", | |
| "image/webp", "image/bmp", "image/tiff", | |
| ]) | |
| ASPECT_RATIO_PRESETS = { | |
| "16:9 (Landscape)": {"width": 1344, "height": 768}, | |
| "9:16 (Portrait)": {"width": 768, "height": 1344}, | |
| "1:1 (Square)": {"width": 1024, "height": 1024}, | |
| "4:3 (Classic TV)": {"width": 1152, "height": 864}, | |
| "3:4 (Classic Portrait)": {"width": 864, "height": 1152}, | |
| "3:2 (Landscape)": {"width": 1248, "height": 832}, | |
| "2:3 (Portrait)": {"width": 832, "height": 1248}, | |
| } | |
| def _parse_image_param(image_param: Any) -> Any: | |
| """Parse a Base64 Data URI, HTTP/HTTPS URL, or PIL.Image into a PIL Image object.""" | |
| if isinstance(image_param, Image.Image): | |
| return image_param | |
| if not isinstance(image_param, str) or not image_param.strip(): | |
| return None | |
| image_param = image_param.strip() | |
| if os.path.exists(image_param): | |
| return Image.open(image_param) | |
| if image_param.startswith("http://") or image_param.startswith("https://"): | |
| return _download_image_from_url(image_param) | |
| if image_param.startswith("data:image/"): | |
| _, encoded = image_param.split(",", 1) if "," in image_param else ("", image_param) | |
| data = base64.b64decode(encoded) | |
| return Image.open(io.BytesIO(data)) | |
| if len(image_param) > 100: | |
| try: | |
| data = base64.b64decode(image_param) | |
| return Image.open(io.BytesIO(data)) | |
| except Exception: | |
| pass | |
| raise ValueError( | |
| "Invalid image parameter format. Expected a file path, Base64 Data URI, or an HTTP/HTTPS URL." | |
| ) | |
| def _download_image_from_url(url: str) -> Image.Image: | |
| """Download an image from an HTTP/HTTPS URL and return it as a PIL Image.""" | |
| req = urllib.request.Request(url, headers={"User-Agent": "H3-VideoGen-MCP/1.0"}) | |
| try: | |
| with urllib.request.urlopen(req, timeout=_IMAGE_DOWNLOAD_TIMEOUT) as resp: | |
| content_type = resp.headers.get("Content-Type", "").split(";")[0].strip().lower() | |
| if content_type and content_type not in _ALLOWED_IMAGE_CONTENT_TYPES: | |
| raise ValueError( | |
| f"URL returned non-image Content-Type '{content_type}'. " | |
| f"Expected one of: {', '.join(sorted(_ALLOWED_IMAGE_CONTENT_TYPES))}." | |
| ) | |
| content_length = resp.headers.get("Content-Length") | |
| if content_length and int(content_length) > _MAX_IMAGE_DOWNLOAD_BYTES: | |
| raise ValueError( | |
| f"Image at URL is too large ({int(content_length)} bytes). " | |
| f"Maximum allowed size is {_MAX_IMAGE_DOWNLOAD_BYTES} bytes." | |
| ) | |
| chunks = [] | |
| total = 0 | |
| while True: | |
| chunk = resp.read(8192) | |
| if not chunk: | |
| break | |
| total += len(chunk) | |
| if total > _MAX_IMAGE_DOWNLOAD_BYTES: | |
| raise ValueError(f"Image download exceeded maximum allowed size of {_MAX_IMAGE_DOWNLOAD_BYTES} bytes.") | |
| chunks.append(chunk) | |
| data = b"".join(chunks) | |
| except urllib.error.URLError as e: | |
| raise ValueError(f"Failed to download image from URL: {e}") from e | |
| except urllib.error.HTTPError as e: | |
| raise ValueError(f"HTTP error {e.code} when downloading image from URL: {e.reason}") from e | |
| if not data: | |
| raise ValueError("Downloaded image data is empty.") | |
| return Image.open(io.BytesIO(data)) | |
| def _download_file_from_url(url: str, suffix: str = ".mp4", max_bytes: int = _MAX_VIDEO_DOWNLOAD_BYTES, timeout: int = _VIDEO_DOWNLOAD_TIMEOUT) -> str: | |
| """Download a media file from an HTTP/HTTPS URL and save it to a temporary local path.""" | |
| req = urllib.request.Request(url, headers={"User-Agent": "H3-VideoGen-MCP/1.0"}) | |
| input_dir = os.path.join(_PROJECT_ROOT, "input") | |
| os.makedirs(input_dir, exist_ok=True) | |
| temp_path = os.path.join(input_dir, f"mcp_download_{uuid.uuid4().hex[:10]}{suffix}") | |
| try: | |
| with urllib.request.urlopen(req, timeout=timeout) as resp: | |
| content_length = resp.headers.get("Content-Length") | |
| if content_length and int(content_length) > max_bytes: | |
| raise ValueError( | |
| f"File at URL is too large ({int(content_length)} bytes). " | |
| f"Maximum allowed size is {max_bytes} bytes." | |
| ) | |
| total = 0 | |
| with open(temp_path, "wb") as f: | |
| while True: | |
| chunk = resp.read(65536) | |
| if not chunk: | |
| break | |
| total += len(chunk) | |
| if total > max_bytes: | |
| raise ValueError(f"File download exceeded maximum allowed size of {max_bytes} bytes.") | |
| f.write(chunk) | |
| return temp_path | |
| except Exception as e: | |
| if os.path.exists(temp_path): | |
| try: | |
| os.remove(temp_path) | |
| except Exception: | |
| pass | |
| raise ValueError(f"Failed to download file from URL '{url}': {e}") from e | |
| def _parse_video_param(video_param: Any) -> Any: | |
| """Parse a file path, Base64 Data URI, or an HTTP/HTTPS URL into a local video file path.""" | |
| if not isinstance(video_param, str) or not video_param.strip(): | |
| return None | |
| video_param = video_param.strip() | |
| if os.path.exists(video_param): | |
| return video_param | |
| if video_param.startswith("http://") or video_param.startswith("https://"): | |
| parsed_url = urllib.parse.urlparse(video_param) | |
| ext = os.path.splitext(parsed_url.path)[1] or ".mp4" | |
| return _download_file_from_url(video_param, suffix=ext, max_bytes=_MAX_VIDEO_DOWNLOAD_BYTES, timeout=_VIDEO_DOWNLOAD_TIMEOUT) | |
| if video_param.startswith("data:video/"): | |
| _, encoded = video_param.split(",", 1) if "," in video_param else ("", video_param) | |
| data = base64.b64decode(encoded) | |
| input_dir = os.path.join(_PROJECT_ROOT, "input") | |
| os.makedirs(input_dir, exist_ok=True) | |
| temp_path = os.path.join(input_dir, f"mcp_upload_{uuid.uuid4().hex[:10]}.mp4") | |
| with open(temp_path, "wb") as f: | |
| f.write(data) | |
| return temp_path | |
| return video_param | |
| def _parse_audio_param(audio_param: Any) -> Any: | |
| """Parse a file path, Base64 Data URI, or an HTTP/HTTPS URL into a local audio file path.""" | |
| if not isinstance(audio_param, str) or not audio_param.strip(): | |
| return None | |
| audio_param = audio_param.strip() | |
| if os.path.exists(audio_param): | |
| return audio_param | |
| if audio_param.startswith("http://") or audio_param.startswith("https://"): | |
| parsed_url = urllib.parse.urlparse(audio_param) | |
| ext = os.path.splitext(parsed_url.path)[1] or ".mp3" | |
| return _download_file_from_url(audio_param, suffix=ext, max_bytes=_MAX_AUDIO_DOWNLOAD_BYTES, timeout=_AUDIO_DOWNLOAD_TIMEOUT) | |
| if audio_param.startswith("data:audio/"): | |
| _, encoded = audio_param.split(",", 1) if "," in audio_param else ("", audio_param) | |
| data = base64.b64decode(encoded) | |
| input_dir = os.path.join(_PROJECT_ROOT, "input") | |
| os.makedirs(input_dir, exist_ok=True) | |
| temp_path = os.path.join(input_dir, f"mcp_upload_{uuid.uuid4().hex[:10]}.mp3") | |
| with open(temp_path, "wb") as f: | |
| f.write(data) | |
| return temp_path | |
| return audio_param | |
| def _load_yaml(filepath: str) -> dict: | |
| """Safely load a YAML file.""" | |
| if not os.path.exists(filepath): | |
| print(f"Warning: YAML file not found: {filepath}") | |
| return {} | |
| with open(filepath, "r", encoding="utf-8") as f: | |
| return yaml.safe_load(f) or {} | |
| _GLOBAL_OPTIONAL_INPUTS_SCHEMA = { | |
| "seed": { | |
| "type": "integer", | |
| "default": -1, | |
| "description": "Random seed for video generation (-1 for random seed, >=0 for deterministic reproduction)." | |
| }, | |
| "zero_gpu_duration": { | |
| "type": "integer", | |
| "default": 120, | |
| "minimum": 1, | |
| "description": "GPU execution time quota allocation in seconds on HuggingFace ZeroGPU spaces (default: 120)." | |
| }, | |
| "loras": { | |
| "type": "array", | |
| "description": "List of LoRA configurations to apply. E.g., [{'source': 'Hugging Face', 'id_or_url': 'repo/lora.safetensors', 'scale': 1.0}] or [{'lora_name': 'my_lora.safetensors', 'strength_model': 1.0, 'strength_clip': 1.0}].", | |
| "items": { | |
| "type": "object" | |
| } | |
| }, | |
| "h3_controlnets": { | |
| "type": "array", | |
| "description": "List of H3 ControlNet configurations. E.g., [{'video': 'https://example.com/pose.mp4', 'control_net_name': 'minimax_h3_fun_controlnet_union_pruned_int8_convrot.safetensors', 'strength': 1.0, 'start_percent': 0.0, 'end_percent': 1.0}].", | |
| "items": { | |
| "type": "object" | |
| } | |
| }, | |
| "control_video": { | |
| "type": "string", | |
| "description": "Convenience parameter: URL, file path, or Base64 Data URI of a single control video for H3 ControlNet." | |
| }, | |
| "control_strength": { | |
| "type": "number", | |
| "default": 1.0, | |
| "description": "Strength for single convenience control video (default: 1.0)." | |
| }, | |
| "control_net_name": { | |
| "type": "string", | |
| "default": "minimax_h3_fun_controlnet_union_pruned_int8_convrot.safetensors", | |
| "description": "Model filename for H3 ControlNet (default: minimax_h3_fun_controlnet_union_pruned_int8_convrot.safetensors)." | |
| }, | |
| "h3_guides": { | |
| "type": "array", | |
| "description": "List of MiniMax H3 timeline Keyframe Guide configurations (Add Guide for MiniMax H3). E.g., [{'image': 'https://example.com/frame.png', 'video': 'https://example.com/clip.mp4', 'audio': 'https://example.com/voice.mp3', 'frame_idx': 36, 'time_seconds': 1.5}].", | |
| "items": { | |
| "type": "object" | |
| } | |
| } | |
| } | |
| _COMMON_OPTIONAL_INPUTS = [ | |
| "seed", "zero_gpu_duration", "loras", "h3_controlnets", "control_video", "control_strength", "control_net_name", "h3_guides" | |
| ] | |
| _TASK_DEFINITIONS = [ | |
| { | |
| "task_type": "t2va", | |
| "display_name": "Text-to-Video (H3 FL2VA)", | |
| "description": "Generate videos from text prompt without input frames. Supports LoRAs and H3 ControlNet.", | |
| "required_inputs": ["prompt", "width", "height", "duration"], | |
| "optional_inputs": _COMMON_OPTIONAL_INPUTS, | |
| "optional_inputs_schema": _GLOBAL_OPTIONAL_INPUTS_SCHEMA, | |
| "aspect_ratio_presets": ASPECT_RATIO_PRESETS, | |
| "example_json_params": { | |
| "task_type": "t2va", | |
| "prompt": "A futuristic city with flying cars at sunset, cinematic 4k", | |
| "width": 1344, | |
| "height": 768, | |
| "duration": 5.0, | |
| "seed": -1, | |
| "zero_gpu_duration": 120 | |
| } | |
| }, | |
| { | |
| "task_type": "i2va", | |
| "display_name": "Image-to-Video (H3 FL2VA)", | |
| "description": "Generate videos starting from a single initial image frame (first frame). Supports LoRAs and H3 ControlNet.", | |
| "required_inputs": ["prompt", "width", "height", "duration", "first_frame_image"], | |
| "optional_inputs": _COMMON_OPTIONAL_INPUTS, | |
| "optional_inputs_schema": _GLOBAL_OPTIONAL_INPUTS_SCHEMA, | |
| "aspect_ratio_presets": ASPECT_RATIO_PRESETS, | |
| "example_json_params": { | |
| "task_type": "i2va", | |
| "prompt": "A majestic lion standing on a rock looking at the horizon", | |
| "width": 1344, | |
| "height": 768, | |
| "duration": 5.0, | |
| "first_frame_image": "https://example.com/first_frame.png", | |
| "seed": -1, | |
| "zero_gpu_duration": 120 | |
| } | |
| }, | |
| { | |
| "task_type": "flf2va", | |
| "display_name": "First & Last Frame-to-Video (H3 FL2VA)", | |
| "description": "Generate videos guided by both a starting frame (first frame) and ending frame (last frame). Supports LoRAs and H3 ControlNet.", | |
| "required_inputs": ["prompt", "width", "height", "duration", "first_frame_image", "last_frame_image"], | |
| "optional_inputs": _COMMON_OPTIONAL_INPUTS, | |
| "optional_inputs_schema": _GLOBAL_OPTIONAL_INPUTS_SCHEMA, | |
| "aspect_ratio_presets": ASPECT_RATIO_PRESETS, | |
| "example_json_params": { | |
| "task_type": "flf2va", | |
| "prompt": "Smooth transition of a flower blooming from start to finish", | |
| "width": 1344, | |
| "height": 768, | |
| "duration": 5.0, | |
| "first_frame_image": "https://example.com/first_frame.png", | |
| "last_frame_image": "https://example.com/last_frame.png", | |
| "seed": -1, | |
| "zero_gpu_duration": 120 | |
| } | |
| }, | |
| { | |
| "task_type": "ref2va", | |
| "display_name": "Reference-to-Video (H3 REF2VA)", | |
| "description": "Generate videos with multi-modal references (up to 9 ref images, 3 ref videos, and 3 ref audios). Supports LoRAs, H3 ControlNet, and timeline Keyframe Guides (Add Guide for MiniMax H3).", | |
| "required_inputs": ["prompt", "width", "height", "duration"], | |
| "optional_inputs": [ | |
| *_COMMON_OPTIONAL_INPUTS, | |
| "ref_image1", "ref_image2", "ref_image3", "ref_image4", "ref_image5", "ref_image6", "ref_image7", "ref_image8", "ref_image9", | |
| "ref_video1", "ref_video2", "ref_video3", | |
| "ref_audio1", "ref_audio2", "ref_audio3", | |
| "h3_guides" | |
| ], | |
| "optional_inputs_schema": _GLOBAL_OPTIONAL_INPUTS_SCHEMA, | |
| "aspect_ratio_presets": ASPECT_RATIO_PRESETS, | |
| "example_json_params": { | |
| "task_type": "ref2va", | |
| "prompt": "A character dancing in a cyberpunk room guided by reference media", | |
| "width": 1344, | |
| "height": 768, | |
| "duration": 5.0, | |
| "ref_image1": "https://example.com/ref_character.png", | |
| "ref_video1": "https://example.com/ref_motion.mp4", | |
| "ref_audio1": "https://example.com/ref_bgm.mp3", | |
| "seed": -1, | |
| "zero_gpu_duration": 120 | |
| } | |
| } | |
| ] | |
| _TASKS_DB: Dict[str, Dict[str, Any]] = {} | |
| class DummyProgress: | |
| def __call__(self, progress=0.0, desc=None): | |
| pass | |
| def _get_public_base_url() -> str: | |
| """Auto-resolve the publicly accessible base URL.""" | |
| public_url = os.getenv("PUBLIC_URL") or os.getenv("BASE_URL") | |
| if public_url: | |
| return public_url.rstrip("/") | |
| space_host = os.getenv("SPACE_HOST") | |
| if space_host: | |
| if not space_host.startswith("http://") and not space_host.startswith("https://"): | |
| return f"https://{space_host}" | |
| return space_host.rstrip("/") | |
| try: | |
| from core.settings import GRADIO_SERVER_NAME, SERVER_PORT | |
| except ImportError: | |
| GRADIO_SERVER_NAME = "127.0.0.1" | |
| SERVER_PORT = 7860 | |
| server_name = os.getenv("GRADIO_SERVER_NAME", GRADIO_SERVER_NAME) | |
| if server_name == "0.0.0.0": | |
| server_name = "127.0.0.1" | |
| port = os.getenv("GRADIO_SERVER_PORT", str(SERVER_PORT)) | |
| return f"http://{server_name}:{port}" | |
| def _execute_imagegen_pipeline(task_id: str, params: dict): | |
| """Execute the MiniMax H3 video generation pipeline in the background and update _TASKS_DB.""" | |
| start_time = time.time() | |
| try: | |
| _TASKS_DB[task_id]["status"] = "processing" | |
| _TASKS_DB[task_id]["progress"] = 10 | |
| _TASKS_DB[task_id]["updated_at"] = int(start_time) | |
| task_type = params.get("task_type", "t2va") | |
| prompt = params.get("prompt", "") | |
| width = int(params.get("width", 1344)) | |
| height = int(params.get("height", 768)) | |
| duration = float(params.get("duration", 5.0)) | |
| seed = int(params.get("seed", -1)) | |
| zero_gpu_duration = int(params.get("zero_gpu_duration", 120)) | |
| # Process LoRAs if provided | |
| loras_param = params.get("loras") | |
| loras_list = loras_param if isinstance(loras_param, list) else [] | |
| # Process H3 ControlNets if provided | |
| h3_cns_param = params.get("h3_controlnets") | |
| parsed_cns = [] | |
| if h3_cns_param and isinstance(h3_cns_param, list): | |
| for item in h3_cns_param: | |
| if isinstance(item, dict): | |
| vid_raw = item.get("video") or item.get("control_video") or item.get("file") | |
| vid_parsed = _parse_video_param(vid_raw) if vid_raw else None | |
| if vid_parsed: | |
| new_item = dict(item) | |
| new_item["video"] = vid_parsed | |
| parsed_cns.append(new_item) | |
| elif params.get("control_video"): | |
| vid_raw = params.get("control_video") | |
| vid_parsed = _parse_video_param(vid_raw) if vid_raw else None | |
| if vid_parsed: | |
| parsed_cns.append({ | |
| "video": vid_parsed, | |
| "control_net_name": params.get("control_net_name") or "minimax_h3_fun_controlnet_union_pruned_int8_convrot.safetensors", | |
| "strength": float(params.get("control_strength", 1.0)), | |
| "start_percent": float(params.get("control_start_percent", 0.0)), | |
| "end_percent": float(params.get("control_end_percent", 1.0)), | |
| }) | |
| parsed_guides = [] | |
| raw_guides = params.get("h3_guides") or params.get("guides") | |
| if raw_guides and isinstance(raw_guides, list): | |
| for item in raw_guides: | |
| if isinstance(item, dict): | |
| guide_entry = {} | |
| if item.get("image"): | |
| guide_entry["image"] = _parse_image_param(item["image"]) | |
| if item.get("video"): | |
| guide_entry["video"] = _parse_video_param(item["video"]) | |
| if item.get("audio"): | |
| guide_entry["audio"] = _parse_audio_param(item["audio"]) | |
| if item.get("frame_idx") is not None: | |
| guide_entry["frame_idx"] = item["frame_idx"] | |
| if item.get("time_seconds") is not None: | |
| guide_entry["time_seconds"] = item["time_seconds"] | |
| if guide_entry: | |
| parsed_guides.append(guide_entry) | |
| if task_type in ("t2va", "i2va", "flf2va"): | |
| from ui.h3_logic import process_inputs as process_h3_inputs | |
| first_frame_raw = params.get("first_frame_image") or params.get("first_frame") | |
| last_frame_raw = params.get("last_frame_image") or params.get("last_frame") | |
| first_frame_pil = _parse_image_param(first_frame_raw) if first_frame_raw else None | |
| last_frame_pil = _parse_image_param(last_frame_raw) if last_frame_raw else None | |
| ui_inputs = { | |
| "task": task_type.upper(), | |
| "first_frame": first_frame_pil, | |
| "last_frame": last_frame_pil, | |
| "prompt": prompt, | |
| "resolution": "720p", | |
| "aspect_ratio": "Custom", | |
| "width": width, | |
| "height": height, | |
| "duration": duration, | |
| "seed": seed, | |
| "zero_gpu_duration": zero_gpu_duration, | |
| "batch_count": 1, | |
| "loras": loras_list, | |
| "h3_controlnets": parsed_cns, | |
| "h3_guides": parsed_guides, | |
| } | |
| elif task_type == "ref2va": | |
| from ui.h3_logic import process_inputs as process_h3_inputs | |
| ref_images = [] | |
| for i in range(1, 10): | |
| img_val = params.get(f"ref_image{i}") | |
| if img_val: | |
| pil_img = _parse_image_param(img_val) | |
| if pil_img: | |
| ref_images.append(pil_img) | |
| if not ref_images and "ref_images" in params and isinstance(params["ref_images"], list): | |
| for img_val in params["ref_images"]: | |
| if img_val: | |
| pil_img = _parse_image_param(img_val) | |
| if pil_img: | |
| ref_images.append(pil_img) | |
| ref_videos = [] | |
| for i in range(1, 4): | |
| vid_val = params.get(f"ref_video{i}") | |
| if vid_val: | |
| parsed_vid = _parse_video_param(vid_val) | |
| if parsed_vid: | |
| ref_videos.append(parsed_vid) | |
| if not ref_videos and "ref_videos" in params and isinstance(params["ref_videos"], list): | |
| for vid_val in params["ref_videos"]: | |
| if vid_val: | |
| parsed_vid = _parse_video_param(vid_val) | |
| if parsed_vid: | |
| ref_videos.append(parsed_vid) | |
| ref_audios = [] | |
| for i in range(1, 4): | |
| aud_val = params.get(f"ref_audio{i}") | |
| if aud_val: | |
| parsed_aud = _parse_audio_param(aud_val) | |
| if parsed_aud: | |
| ref_audios.append(parsed_aud) | |
| if not ref_audios and "ref_audios" in params and isinstance(params["ref_audios"], list): | |
| for aud_val in params["ref_audios"]: | |
| if aud_val: | |
| parsed_aud = _parse_audio_param(aud_val) | |
| if parsed_aud: | |
| ref_audios.append(parsed_aud) | |
| ui_inputs = { | |
| "task": "REF2VA", | |
| "prompt": prompt, | |
| "resolution": "720p", | |
| "aspect_ratio": "Custom", | |
| "width": width, | |
| "height": height, | |
| "duration": duration, | |
| "seed": seed, | |
| "zero_gpu_duration": zero_gpu_duration, | |
| "batch_count": 1, | |
| "ref_images": ref_images, | |
| "ref_videos": ref_videos, | |
| "ref_audios": ref_audios, | |
| "loras": loras_list, | |
| "h3_controlnets": parsed_cns, | |
| "h3_guides": parsed_guides, | |
| } | |
| else: | |
| raise ValueError(f"Unsupported task_type '{task_type}'") | |
| _TASKS_DB[task_id]["progress"] = 50 | |
| from core.generation_logic import generate_h3_video_wrapper | |
| output = generate_h3_video_wrapper(process_h3_inputs, ui_inputs, progress=DummyProgress()) | |
| try: | |
| from core.settings import OUTPUT_DIR | |
| except ImportError: | |
| OUTPUT_DIR = os.path.join(_PROJECT_ROOT, "output") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| import tempfile | |
| import gradio.processing_utils as pu | |
| gradio_cache_dir = os.path.join(tempfile.gettempdir(), "gradio") | |
| os.makedirs(gradio_cache_dir, exist_ok=True) | |
| base_url = _get_public_base_url() | |
| videos = [] | |
| raw_list = output if isinstance(output, list) else ([output] if output else []) | |
| for idx, item in enumerate(raw_list): | |
| target_path = None | |
| if isinstance(item, str) and os.path.exists(item): | |
| target_path = item | |
| if target_path: | |
| try: | |
| cached_path = pu.save_file_to_cache(target_path, cache_dir=gradio_cache_dir) | |
| abs_path = os.path.abspath(cached_path).replace("\\", "/") | |
| except Exception as e: | |
| print(f"Warning: Failed to cache video file to Gradio temp dir: {e}") | |
| abs_path = os.path.abspath(target_path).replace("\\", "/") | |
| url = f"{base_url}/gradio_api/file={urllib.parse.quote(abs_path)}" | |
| videos.append(url) | |
| elif item: | |
| videos.append(str(item)) | |
| execution_time = round(time.time() - start_time, 2) | |
| _TASKS_DB[task_id]["status"] = "completed" | |
| _TASKS_DB[task_id]["progress"] = 100 | |
| _TASKS_DB[task_id]["completed_at"] = int(time.time()) | |
| _TASKS_DB[task_id]["result"] = { | |
| "task_type": task_type, | |
| "videos": videos, | |
| "video_path": raw_list[0] if raw_list and isinstance(raw_list[0], str) else None, | |
| "seed": seed, | |
| "width": width, | |
| "height": height, | |
| "duration": duration, | |
| "execution_time_seconds": execution_time, | |
| } | |
| except Exception as e: | |
| _TASKS_DB[task_id]["status"] = "failed" | |
| _TASKS_DB[task_id]["progress"] = 0 | |
| _TASKS_DB[task_id]["failed_at"] = int(time.time()) | |
| _TASKS_DB[task_id]["error"] = { | |
| "code": "EXECUTION_ERROR", | |
| "message": str(e), | |
| } | |