from __future__ import annotations import secrets import json import threading import time from datetime import datetime, timezone from typing import Callable import torch from PIL import Image from .model_resolver import prepare_models_root from .realesrgan_upscaler import ResidentHDUpscaler from .space_config import SpaceConfig from .space_postprocess import ( apply_rife_seam, close_rife_model, prepare_rife_model, save_frame_bundle, ) from .wanvideo_loop_runtime import WrapperLoopRuntime def _diag(event: str, **fields) -> None: payload = { "ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"), "event": event, **fields, } print(f"[WAN_SERVICE] {json.dumps(payload, sort_keys=True)}", flush=True) class LoopGeneratorService: def __init__(self, config: SpaceConfig) -> None: self.config = config started = time.perf_counter() _diag("service.init.start") models_root, names = prepare_models_root() self.runtime = WrapperLoopRuntime( models_root=models_root, high_model_name=names["high"], low_model_name=names["low"], clip_name=names["text"], int8_clip_name=names["text_int8"], vae_name=names["vae"], sampler_name=config.sampler, scheduler_mode=config.scheduler, split_step=config.split_step, riflex_k=0, loop_shift_skip=0, loop_start_percent=0.0, loop_end_percent=1.0, start_latent_strength=config.start_strength, end_latent_strength=config.end_strength, end_temporal_mask_strength=config.end_mask_strength, decode_end_image_hint=True, fun_or_fl2v_model=False, zero_end_latent_conditioning=False, end_latent_conditioning_strength=1.0, low_pass_end_conditioning_strength=1.0, custom_sigmas=(), attention_mode="sdpa", text_encoder_quantization=config.text_encoder_quantization, global_resident_models=config.global_resident_models, vae_tiling=config.vae_tiling, ) if config.use_rife: prepare_rife_model() self.upscaler = ResidentHDUpscaler() self._job_lock = threading.Lock() _diag("service.init.done", elapsed_s=round(time.perf_counter() - started, 3)) def cleanup_job(self) -> None: self.runtime.cleanup_job() def close(self) -> None: """Destroy process-global resources. Do not call after a normal job.""" self.upscaler.close() close_rife_model() self.runtime.shutdown() def generate_iter( self, image: Image.Image, prompt: str, progress_callback: Callable[..., None] | None = None, ): if image is None: raise ValueError("Upload an image.") if not (prompt or "").strip(): raise ValueError("Enter a prompt.") if not self._job_lock.acquire(blocking=False): raise RuntimeError("The generator is already processing another request.") image = image.convert("RGB") frames = None try: self.runtime.cleanup_job() with torch.inference_mode(): for stage_result in self.runtime.generate_segment_iter( prompt=prompt, negative_prompt="", start_image=image, end_image=image, width=self.config.width, height=self.config.height, num_frames=self.config.frame_count, steps=self.config.steps, cfg=self.config.cfg, shift=self.config.shift, seed=secrets.randbits(63), progress_callback=progress_callback, ): if isinstance(stage_result, dict) and "stage" in stage_result: yield stage_result else: frames = stage_result if frames is None: raise RuntimeError("Frame generation ended without decoded frames.") metrics = dict(self.runtime.last_metrics) if self.config.use_rife: if progress_callback is not None: progress_callback(0.87, desc="Smoothing loop seam…") yield {"stage": "Smoothing loop seam…"} if torch.cuda.is_available(): torch.cuda.synchronize() rife_started = time.perf_counter() _diag("rife.start", input_frames=int(frames.shape[0])) frames = apply_rife_seam(frames, self.config.rife_frames) if torch.cuda.is_available(): torch.cuda.synchronize() metrics["rife_s"] = time.perf_counter() - rife_started if torch.cuda.is_available(): metrics["peak_vram_gib"] = torch.cuda.max_memory_allocated() / (1024**3) _diag( "rife.done", elapsed_s=round(metrics["rife_s"], 3), output_frames=int(frames.shape[0]), output_device=str(frames.device), ) if progress_callback is not None: progress_callback(0.91, desc="Upscaling to HD…") yield {"stage": "Upscaling to HD…"} if torch.cuda.is_available(): torch.cuda.synchronize() upscale_started = time.perf_counter() _diag( "upscale.start", input_width=int(frames.shape[2]), input_height=int(frames.shape[1]), input_frames=int(frames.shape[0]), ) frames = self.upscaler.upscale_to_hd(frames) if torch.cuda.is_available(): torch.cuda.synchronize() upscale_s = time.perf_counter() - upscale_started metrics["upscale_s"] = upscale_s if torch.cuda.is_available(): metrics["peak_vram_gib"] = torch.cuda.max_memory_allocated() / (1024**3) _diag( "upscale.done", elapsed_s=round(upscale_s, 3), output_width=int(frames.shape[2]), output_height=int(frames.shape[1]), output_frames=int(frames.shape[0]), output_device=str(frames.device), ) print(f"[SPACE_METRICS_FINAL] {json.dumps(metrics, sort_keys=True)}", flush=True) if progress_callback is not None: progress_callback(0.95, desc="Preparing frames…") yield {"stage": "Preparing frames…"} bundle_started = time.perf_counter() _diag("frame_bundle.start", input_device=str(frames.device)) bundle = save_frame_bundle(frames) _diag("frame_bundle.done", elapsed_s=round(time.perf_counter() - bundle_started, 3)) yield {"bundle": bundle} finally: frames = None self.runtime.cleanup_job() self._job_lock.release()