Spaces:
Running on Zero
Running on Zero
| import spaces | |
| import os | |
| import sys | |
| import copy | |
| import time | |
| import uuid | |
| import tempfile | |
| import torch | |
| import torch._dynamo | |
| import gradio as gr | |
| from tqdm import tqdm | |
| from huggingface_hub import HfApi | |
| from diffusers.pipelines.wan.pipeline_wan_i2v import WanImageToVideoPipeline | |
| from diffusers.utils.export_utils import export_to_video | |
| from torchao.quantization import quantize_, Float8DynamicActivationFloat8WeightConfig, Int8WeightOnlyConfig | |
| import config | |
| import aoti | |
| import lora_loader | |
| from image_utils import resize_image, resize_and_crop_to_match, get_num_frames | |
| from rife_interp import rife_model, interpolate_bits, create_classic_boomerang_loop, create_ending_boomerang_loop, create_dynamic_boomerang_loop, create_adaptive_speed_ramping, call_sulphur_rife_api, clear_vram, is_cuda_usable | |
| from face_swapper import swap_face_in_frames, swap_face_in_single_image | |
| from prompt_relay import PromptRelayManager | |
| pipe = WanImageToVideoPipeline.from_pretrained( | |
| config.MODEL_ID, | |
| torch_dtype=torch.bfloat16, | |
| ).to('cuda') | |
| original_scheduler = copy.deepcopy(pipe.scheduler) | |
| for i, lora in enumerate(config.LORA_MODELS): | |
| name_high_tr = lora["high_tr"].split(".")[0].split("/")[-1] + "Hh" | |
| name_low_tr = lora["low_tr"].split(".")[0].split("/")[-1] + "Ll" | |
| try: | |
| pipe.load_lora_weights(lora["repo_id"], weight_name=lora["high_tr"], adapter_name=name_high_tr) | |
| kwargs_lora = {"load_into_transformer_2": True} | |
| pipe.load_lora_weights(lora["repo_id"], weight_name=lora["low_tr"], adapter_name=name_low_tr, **kwargs_lora) | |
| pipe.set_adapters([name_high_tr, name_low_tr], adapter_weights=[1.0, 1.0]) | |
| pipe.fuse_lora(adapter_names=[name_high_tr], lora_scale=lora["high_scale"], components=["transformer"]) | |
| pipe.fuse_lora(adapter_names=[name_low_tr], lora_scale=lora["low_scale"], components=["transformer_2"]) | |
| pipe.unload_lora_weights() | |
| print(f"Applied: {lora['high_tr']}, hs={lora['high_scale']}/ls={lora['low_scale']}, {i+1}/{len(config.LORA_MODELS)}") | |
| except Exception as e: | |
| print("Error:", str(e)) | |
| print("Failed LoRA:", name_high_tr) | |
| pipe.unload_lora_weights() | |
| quantize_(pipe.text_encoder, Int8WeightOnlyConfig()) | |
| torch._dynamo.reset() | |
| quantize_(pipe.transformer, Float8DynamicActivationFloat8WeightConfig()) | |
| torch._dynamo.reset() | |
| quantize_(pipe.transformer_2, Float8DynamicActivationFloat8WeightConfig()) | |
| torch._dynamo.reset() | |
| spaces.aoti_load(module=pipe.transformer, repo_id='thornmaze/WanTransformer3DModel-sm120-cu130-raa') | |
| spaces.aoti_load(module=pipe.transformer_2, repo_id='thornmaze/WanTransformer3DModel-sm120-cu130-raa') | |
| def get_inference_duration( | |
| resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames, | |
| guidance_scale, guidance_scale_2, current_seed, scheduler_name, flow_shift, | |
| frame_multiplier, quality, duration_seconds, safe_mode=False, lora_groups=None, | |
| custom_lora_url="", custom_lora_scale=1.0, enable_prompt_relay=False, | |
| relay_prompt_schedule="", noise_temperature=1.0, extra_gpu_buffer=0, *args, **kwargs | |
| ): | |
| width, height = resized_image.size | |
| # Non-linear 3D attention memory & sequence scaling for Wan 2.2 frame count | |
| frame_ratio = (num_frames / 81.0) ** 1.38 | |
| spatial_ratio = (width * height) / (832 * 624) | |
| factor = frame_ratio * spatial_ratio | |
| # Calibrated base step duration: 9.8s for <=4.0s (65 frames) to yield ~21s reservation (saving quota while covering ~18.5-19.2s GPU compute) | |
| BASE_STEP_DURATION = 9.8 if num_frames <= 65 else 11.0 | |
| step_duration = BASE_STEP_DURATION * factor | |
| gen_time = int(steps) * step_duration | |
| # Automatically double reservation duration when Classifier-Free Guidance (GS > 1.0) is active | |
| if float(guidance_scale) > 1.0 or float(guidance_scale_2) > 1.0: | |
| gen_time = gen_time * 2.0 | |
| overhead = 2.0 if num_frames <= 33 else (3.0 if num_frames <= 65 else 5.0) | |
| # Automatically add +3 seconds overhead if custom LoRA is requested | |
| cached_l = str(kwargs.get("cached_lora") or "").strip() | |
| if (custom_lora_url and str(custom_lora_url).strip()) or (cached_l and cached_l != "(None / Disable)"): | |
| overhead += 3.0 | |
| total_time = overhead + gen_time + float(extra_gpu_buffer or 0) | |
| if safe_mode: | |
| total_time = total_time * 1.25 | |
| return max(6, int(total_time) + 1) | |
| def run_inference( | |
| resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames, | |
| guidance_scale, guidance_scale_2, current_seed, scheduler_name, flow_shift, | |
| frame_multiplier, quality, duration_seconds, safe_mode=False, lora_groups=None, | |
| custom_lora_url="", custom_lora_scale=1.0, enable_prompt_relay=False, | |
| relay_prompt_schedule="", noise_temperature=1.0, extra_gpu_buffer=0, progress=gr.Progress(track_tqdm=True) | |
| ): | |
| scheduler_class = config.SCHEDULER_MAP.get(scheduler_name) | |
| if scheduler_class.__name__ != pipe.scheduler.config._class_name or flow_shift != pipe.scheduler.config.get("flow_shift", "shift"): | |
| cfg = copy.deepcopy(original_scheduler.config) | |
| if scheduler_class.__name__ == "FlowMatchEulerDiscreteScheduler": | |
| cfg['shift'] = flow_shift | |
| else: | |
| cfg['flow_shift'] = flow_shift | |
| pipe.scheduler = scheduler_class.from_config(cfg) | |
| clear_vram() | |
| # Prompt Relay: Multi-Event Temporal Routing | |
| active_prompt = prompt | |
| if enable_prompt_relay and relay_prompt_schedule and str(relay_prompt_schedule).strip(): | |
| events = PromptRelayManager.parse_schedule(relay_prompt_schedule, duration_seconds, num_frames) | |
| if events: | |
| print(f"🎬 Prompt Relay Active: {len(events)} temporal events routed across {duration_seconds}s") | |
| event_texts = [f"[{e['start_sec']}s-{e['end_sec']}s]: {e['prompt']}" for e in events] | |
| active_prompt = " ".join([e['prompt'] for e in events]) + " " + prompt | |
| print(f" Combined Relay Prompt: {active_prompt[:100]}...") | |
| task_name = str(uuid.uuid4())[:8] | |
| print(f"Generating {num_frames} frames, task: {task_name}, {duration_seconds}, {resized_image.size}, lora={lora_groups}, custom_url={custom_lora_url}, temp={noise_temperature}") | |
| start = time.time() | |
| lora_loaded = False | |
| if lora_groups: | |
| try: | |
| for idx, name in enumerate(lora_groups): | |
| if name and name != "(None)": | |
| lora_loader.load_lora_to_pipe(pipe, name, adapter_name=f"lora_{idx}") | |
| lora_loaded = True | |
| print(f"LoRA loaded: {lora_groups}") | |
| except Exception as e: | |
| print(f"LoRA warning: {e}") | |
| if custom_lora_url and str(custom_lora_url).strip(): | |
| try: | |
| loaded_custom = lora_loader.load_custom_url_lora( | |
| pipe, str(custom_lora_url).strip(), adapter_name="custom_civitai_lora", scale=float(custom_lora_scale) | |
| ) | |
| if loaded_custom: | |
| lora_loaded = True | |
| else: | |
| gr.Warning("⚠️ Selected LoRA model is incompatible with Wan 2.2! File was automatically purged and video generation reverted to base model.") | |
| except Exception as e: | |
| print(f"Custom LoRA URL error: {e}") | |
| gr.Warning(f"⚠️ Custom LoRA Error: {e}. Video generation reverted to base Wan 2.2 model.") | |
| # Initial Noise Temperature scaling (0 Extra GPU Quota) | |
| latents = None | |
| if float(noise_temperature) != 1.0: | |
| try: | |
| latent_frames = (num_frames - 1) // 4 + 1 | |
| latent_h = resized_image.height // 8 | |
| latent_w = resized_image.width // 8 | |
| gen = torch.Generator(device="cuda").manual_seed(current_seed) | |
| latents = torch.randn( | |
| (1, 16, latent_frames, latent_h, latent_w), | |
| generator=gen, | |
| device="cuda", | |
| dtype=pipe.transformer.dtype | |
| ) * float(noise_temperature) | |
| print(f"🌡️ Noise Temperature applied: {noise_temperature} (latents scaled)") | |
| except Exception as e: | |
| print(f"Noise Temperature notice: {e}") | |
| latents = None | |
| pipe_kwargs = { | |
| "image": resized_image, | |
| "last_image": processed_last_image, | |
| "prompt": active_prompt, | |
| "negative_prompt": negative_prompt, | |
| "height": resized_image.height, | |
| "width": resized_image.width, | |
| "num_frames": num_frames, | |
| "guidance_scale": float(guidance_scale), | |
| "guidance_scale_2": float(guidance_scale_2), | |
| "num_inference_steps": int(steps), | |
| "generator": torch.Generator(device="cuda").manual_seed(current_seed), | |
| "output_type": "np" | |
| } | |
| if latents is not None: | |
| pipe_kwargs["latents"] = latents | |
| result = pipe(**pipe_kwargs) | |
| if lora_loaded: | |
| lora_loader.unload_lora(pipe) | |
| print("gen time passed:", time.time() - start) | |
| gpu_time = round(time.time() - start, 2) | |
| raw_frames_np = result.frames[0] | |
| pipe.scheduler = original_scheduler | |
| del result | |
| clear_vram() | |
| return raw_frames_np, task_name, gpu_time | |
| def generate_video( | |
| input_image, last_image, prompt, steps=4, negative_prompt=config.default_negative_prompt, | |
| duration_seconds=config.MAX_DURATION, guidance_scale=1, guidance_scale_2=1, seed=42, | |
| randomize_seed=False, quality=5, scheduler="UniPCMultistep", flow_shift=6.0, | |
| frame_multiplier=16, motion_extension_mode="⚡ Real-Time RIFE Interpolation (32/64 FPS Ultra-Smooth)", | |
| safe_mode=False, custom_lora_url="", custom_lora_scale=1.0, | |
| enable_prompt_relay=False, relay_prompt_schedule="", | |
| ref_face_image=None, target_gender="Any / All Faces", | |
| play_result_video=True, custom_filename="", noise_temperature=1.0, | |
| enable_vip_rife=False, vip_rife_multiplier="2x", vip_rife_mode="High-FPS Motion Smoothness (FPS Boost)", | |
| vip_rife_upscale=True, | |
| vip_rife_enhance_face=False, | |
| vip_password="", | |
| cached_lora="", | |
| extra_gpu_buffer=0, | |
| request: gr.Request = None, | |
| progress=gr.Progress(track_tqdm=True) | |
| ): | |
| if input_image is None: | |
| raise gr.Error("Please upload an input image.") | |
| hf_user = "Guest" | |
| user_ip = "Unknown" | |
| if request is not None: | |
| try: | |
| if hasattr(request, "headers") and request.headers: | |
| user_ip = ( | |
| request.headers.get("x-forwarded-for") or | |
| request.headers.get("x-real-ip") or | |
| request.headers.get("cf-connecting-ip") or | |
| getattr(getattr(request, "client", None), "host", "Unknown") | |
| ) | |
| if "," in user_ip: | |
| user_ip = user_ip.split(",")[0].strip() | |
| hf_name = ( | |
| request.headers.get("x-hf-user-name") or | |
| request.headers.get("x-hf-user") or | |
| request.headers.get("x-username") or | |
| getattr(request, "username", None) | |
| ) | |
| if hf_name and str(hf_name).strip(): | |
| hf_user = str(hf_name).strip() | |
| elif user_ip and user_ip != "Unknown": | |
| hf_user = f"Guest ({user_ip})" | |
| else: | |
| hf_user = "Guest (Anonymous)" | |
| elif hasattr(request, "username") and request.username: | |
| hf_user = request.username | |
| except Exception as err: | |
| print(f"User identification notice: {err}") | |
| if hf_user == "Guest" and user_ip != "Unknown": | |
| hf_user = f"Guest ({user_ip})" | |
| active_custom_lora = str(custom_lora_url or "").strip() | |
| if not active_custom_lora and cached_lora and str(cached_lora).strip() != "(None / Disable)": | |
| active_custom_lora = str(cached_lora).strip() | |
| # CPU Pre-Download Custom LoRA (Before GPU inference starts to preserve ZeroGPU quota) | |
| if active_custom_lora: | |
| if active_custom_lora.startswith("http://") or active_custom_lora.startswith("https://"): | |
| start_dl = time.time() | |
| print(f"📥 Running CPU Pre-Download for Custom LoRA: {active_custom_lora}...") | |
| try: | |
| lora_path = lora_loader.download_file_from_url(active_custom_lora) | |
| print(f"✅ CPU Pre-Download complete in {time.time() - start_dl:.2f}s: {lora_path}") | |
| except Exception as e: | |
| print(f"❌ CPU Custom LoRA download failed: {e}") | |
| raise gr.Error(f"Gagal mengunduh LoRA dari URL: {e}") | |
| else: | |
| print(f"📦 Using Cached Custom LoRA: {active_custom_lora}") | |
| num_frames = get_num_frames(duration_seconds) | |
| current_seed = int(torch.randint(0, config.MAX_SEED, (1,)).item()) if randomize_seed else int(seed) | |
| resized_image = resize_image(input_image) | |
| processed_last_image = None | |
| if last_image: | |
| processed_last_image = resize_and_crop_to_match(last_image, resized_image) | |
| reserved_time = get_inference_duration( | |
| resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames, | |
| guidance_scale, guidance_scale_2, current_seed, scheduler, flow_shift, | |
| frame_multiplier, quality, duration_seconds, safe_mode, None, | |
| active_custom_lora, custom_lora_scale, enable_prompt_relay, | |
| relay_prompt_schedule, noise_temperature, extra_gpu_buffer, progress, cached_lora=active_custom_lora | |
| ) | |
| raw_frames_np, task_n, gpu_time = run_inference( | |
| resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames, | |
| guidance_scale, guidance_scale_2, current_seed, scheduler, flow_shift, | |
| frame_multiplier, quality, duration_seconds, safe_mode, None, | |
| active_custom_lora, custom_lora_scale, enable_prompt_relay, | |
| relay_prompt_schedule, noise_temperature, extra_gpu_buffer, progress | |
| ) | |
| print(f"GPU complete: {task_n}. Release GPU lock and now processing post-processing on CPU...") | |
| # Motion Extension Technique & Playback FPS | |
| final_fps = config.FIXED_FPS | |
| mode_str = str(motion_extension_mode) | |
| if enable_vip_rife: | |
| print("💎 VIP RIFE Acceleration active: Bypassing local CPU post-processing...") | |
| final_frames = list(raw_frames_np) | |
| final_fps = config.FIXED_FPS | |
| elif "Ending" in mode_str or "Tail" in mode_str: | |
| print("🔂 Processing Ending-Only Boomerang Loop (Real-Speed Tail 1.5s Loop)...") | |
| final_frames = create_ending_boomerang_loop(raw_frames_np) | |
| final_fps = config.FIXED_FPS | |
| elif "Boomerang" in mode_str or "Loop" in mode_str or "Ping-Pong" in mode_str: | |
| print("🔂 Processing Classic Full Boomerang Loop (100% Real-Speed Forward+Reverse)...") | |
| final_frames = create_classic_boomerang_loop(raw_frames_np) | |
| final_fps = config.FIXED_FPS | |
| elif "Ramping" in mode_str or "Curve" in mode_str or "Ease" in mode_str or "Speed" in mode_str: | |
| print("🌊 Processing Adaptive Motion Speed Ramping (Ease-In/Out Curve, Real-Time Speed)...") | |
| final_frames = create_adaptive_speed_ramping(raw_frames_np, multiplier=2) | |
| final_fps = config.FIXED_FPS | |
| elif "Real-Time" in mode_str or "Ultra-Smooth" in mode_str: | |
| frame_factor = max(2, int(frame_multiplier // config.FIXED_FPS)) | |
| calc_fps = int(frame_factor * config.FIXED_FPS) | |
| start = time.time() | |
| print(f"⚡ Processing Real-Time RIFE Interpolation ({calc_fps} FPS)...") | |
| use_cuda = is_cuda_usable() | |
| rife_device = torch.device("cuda" if use_cuda else "cpu") | |
| try: | |
| if use_cuda and hasattr(rife_model, "device"): | |
| rife_model.device() | |
| rife_model.flownet = rife_model.flownet.half() | |
| else: | |
| if hasattr(rife_model, "flownet") and rife_model.flownet is not None: | |
| rife_model.flownet = rife_model.flownet.to(rife_device).float() | |
| except Exception as e: | |
| print(f"RIFE device setup notice: {e}") | |
| final_frames = interpolate_bits(raw_frames_np, multiplier=int(frame_factor)) | |
| final_fps = calc_fps | |
| print("Interpolation time passed:", time.time() - start) | |
| else: | |
| # Classic Slow-Motion RIFE (16 FPS Time-Stretch) | |
| frame_factor = max(2, int(frame_multiplier // config.FIXED_FPS)) | |
| start = time.time() | |
| print(f"🐢 Processing Slow-Motion RIFE Interpolation (16 FPS Time-Stretch, {frame_factor}x)...") | |
| use_cuda = is_cuda_usable() | |
| rife_device = torch.device("cuda" if use_cuda else "cpu") | |
| try: | |
| if use_cuda and hasattr(rife_model, "device"): | |
| rife_model.device() | |
| rife_model.flownet = rife_model.flownet.half() | |
| else: | |
| if hasattr(rife_model, "flownet") and rife_model.flownet is not None: | |
| rife_model.flownet = rife_model.flownet.to(rife_device).float() | |
| except Exception as e: | |
| print(f"RIFE device setup notice: {e}") | |
| final_frames = interpolate_bits(raw_frames_np, multiplier=int(frame_factor)) | |
| final_fps = config.FIXED_FPS | |
| print("Interpolation time passed:", time.time() - start) | |
| # Output Filename Logic | |
| if custom_filename and custom_filename.strip(): | |
| filename = custom_filename.strip() | |
| if not filename.lower().endswith(".mp4"): | |
| filename += ".mp4" | |
| video_path = os.path.join(tempfile.gettempdir(), filename) | |
| else: | |
| with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile: | |
| video_path = tmpfile.name | |
| start = time.time() | |
| with tqdm(total=3, desc="Rendering Media", unit="clip") as pbar: | |
| pbar.update(2) | |
| export_to_video(final_frames, video_path, fps=final_fps, quality=quality) | |
| pbar.update(1) | |
| print(f"Export time passed, {final_fps} FPS:", time.time() - start) | |
| # 💎 VIP Remote RIFE Acceleration (Mutually Exclusive: Bypasses CPU RIFE if Active & Authorized) | |
| if enable_vip_rife: | |
| vip_secret = (config.VIP_PASS or os.environ.get("VIP_PASSWORD", "")).strip() | |
| user_pass = (vip_password or "").strip() | |
| if vip_secret and user_pass != vip_secret: | |
| print("❌ Invalid VIP Password Access Key! Falling back to base output.") | |
| gr.Warning("❌ Invalid VIP Password Access Key! Remote VIP GPU RIFE acceleration was blocked.") | |
| else: | |
| try: | |
| print("💎 VIP Remote RIFE Acceleration authorized! Offloading to remote GPU engine...") | |
| mult_val = 2 | |
| if "4x" in str(vip_rife_multiplier): | |
| mult_val = 4 | |
| elif "8x" in str(vip_rife_multiplier): | |
| mult_val = 8 | |
| is_slow_mo = ("Slow-Motion" in str(vip_rife_mode)) or ("Duration" in str(vip_rife_mode)) | |
| vip_video_res = call_sulphur_rife_api( | |
| video_path=video_path, | |
| multiplier=mult_val, | |
| slow_motion=is_slow_mo, | |
| upscale=vip_rife_upscale, | |
| enhance_face=vip_rife_enhance_face, | |
| progress=progress | |
| ) | |
| if vip_video_res and os.path.exists(vip_video_res): | |
| video_path = vip_video_res | |
| print(f"✅ VIP Remote RIFE Acceleration completed successfully: {video_path}") | |
| else: | |
| print("⚠️ VIP RIFE Remote Acceleration failed or offline. Retaining base output.") | |
| except Exception as e: | |
| print(f"❌ VIP Remote RIFE error notice: {e}") | |
| # ------------------------------------------------------------- | |
| # 💾 Instant Persistent Storage Auto-Save (/data/videos, /data/images, /data/prompts) | |
| # ------------------------------------------------------------- | |
| storage_dir = "/data" | |
| if os.path.exists(storage_dir) and os.path.isdir(storage_dir): | |
| try: | |
| import shutil | |
| v_dir = os.path.join(storage_dir, "videos") | |
| i_dir = os.path.join(storage_dir, "images") | |
| p_dir = os.path.join(storage_dir, "prompts") | |
| os.makedirs(v_dir, exist_ok=True) | |
| os.makedirs(i_dir, exist_ok=True) | |
| os.makedirs(p_dir, exist_ok=True) | |
| v_filename = os.path.basename(video_path) | |
| v_basename = os.path.splitext(v_filename)[0] | |
| img_filename = f"input_{v_basename}.jpg" | |
| prompt_txt_filename = f"prompt_{v_basename}.txt" | |
| # 1. Save Video to /data/videos | |
| if os.path.exists(video_path): | |
| dest_v_path = os.path.join(v_dir, v_filename) | |
| shutil.copy(video_path, dest_v_path) | |
| print(f"💾 [Persistent Storage] Instantly saved video to: {dest_v_path}") | |
| # 2. Save Input Image to /data/images | |
| if input_image is not None: | |
| dest_i_path = os.path.join(i_dir, img_filename) | |
| input_image.convert("RGB").save(dest_i_path, format="JPEG", quality=95) | |
| print(f"💾 [Persistent Storage] Instantly saved input image to: {dest_i_path}") | |
| # 3. Save Prompt Text to /data/prompts | |
| prompt_body = f"user hf : {hf_user}\ngambar : {img_filename}\nprompt : {prompt}" | |
| if enable_prompt_relay and relay_prompt_schedule and str(relay_prompt_schedule).strip(): | |
| prompt_body += f"\n\n[PROMPT RELAY SCHEDULE]\n{relay_prompt_schedule}" | |
| dest_p_path = os.path.join(p_dir, prompt_txt_filename) | |
| with open(dest_p_path, "w", encoding="utf-8") as f: | |
| f.write(prompt_body) | |
| print(f"💾 [Persistent Storage] Instantly saved prompt metadata to: {dest_p_path}") | |
| except Exception as e: | |
| print(f"⚠️ Persistent Storage Auto-Save notice : {e}") | |
| sec_per_step = round(gpu_time / max(1, int(steps)), 2) | |
| gpu_report_html = f""" | |
| <div style="background: rgba(99, 102, 241, 0.12); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 12px; padding: 12px 16px; margin-top: 12px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px;"> | |
| <div style="display: flex; align-items: center; gap: 8px;"> | |
| <span style="font-size: 1.1rem;">⚡</span> | |
| <span style="color: #a5b4fc; font-weight: 700; font-size: 0.92rem;">ZeroGPU Quota Consumed:</span> | |
| <span style="color: #38bdf8; font-weight: 800; font-size: 1.05rem;">{gpu_time:.2f} second</span> | |
| </div> | |
| <div style="display: flex; gap: 12px; font-size: 0.82rem; color: #94a3b8;"> | |
| <span>Quota Reservation: <b>{reserved_time}s</b></span> | |
| <span>Speed: <b>{sec_per_step}s/step</b></span> | |
| </div> | |
| </div> | |
| """ | |
| return (video_path if play_result_video else None), video_path, current_seed, gpu_report_html | |