Spaces:
Running on Zero
Running on Zero
| """Unified GEPARD TTS + LongCat-Video-Avatar Gradio Space. | |
| Startup order is load-bearing for ZeroGPU: | |
| 1. Cache/environment variables are set before any third-party imports. | |
| 2. create_env.setup_dependencies() re-pins transformers before ML imports. | |
| 3. spaces is imported before torch. | |
| 4. Both model stacks are loaded at module level and moved to "cuda" once. | |
| """ | |
| import os | |
| os.environ.setdefault("HF_HOME", "/tmp/huggingface") | |
| os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules") | |
| os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib") | |
| os.environ.setdefault("NUMBA_DISABLE_CUDA", "1") | |
| os.environ.setdefault("GRADIO_SSR_MODE", "false") | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| os.environ.setdefault("OMP_NUM_THREADS", "4") | |
| from create_env import setup_dependencies | |
| setup_dependencies() | |
| try: | |
| import spaces # noqa: E402 | |
| except ImportError: # Local syntax/import checks without the Space runtime. | |
| class _SpacesFallback: | |
| def GPU(*args, **kwargs): | |
| if args and callable(args[0]): | |
| return args[0] | |
| def _wrap(fn): | |
| return fn | |
| return _wrap | |
| spaces = _SpacesFallback() | |
| import hashlib # noqa: E402 | |
| import json # noqa: E402 | |
| import math # noqa: E402 | |
| import shutil # noqa: E402 | |
| import subprocess # noqa: E402 | |
| import sys # noqa: E402 | |
| import tempfile # noqa: E402 | |
| import time # noqa: E402 | |
| import uuid # noqa: E402 | |
| from collections import OrderedDict # noqa: E402 | |
| from pathlib import Path # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| import imageio # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| import soundfile as sf # noqa: E402 | |
| import torch # noqa: E402 | |
| import torch.nn.functional as F # noqa: E402 | |
| from huggingface_hub import hf_hub_download, snapshot_download # noqa: E402 | |
| from PIL import Image, UnidentifiedImageError # noqa: E402 | |
| from gepard_inference.engine import AppConfig, GenerationParams, GepardEngine # noqa: E402 | |
| from interface import MODE_CLONE, MODE_PRESET, build_theme # noqa: E402 | |
| ROOT = Path(__file__).parent.resolve() | |
| CONFIG_PATH = ROOT / "config.yaml" | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| WEIGHTS_DIR = Path(os.environ.get("WEIGHTS_DIR", "weights")) | |
| BASE_DIR = WEIGHTS_DIR / "LongCat-Video" | |
| AVATAR_DIR = WEIGHTS_DIR / "LongCat-Video-Avatar-1.5" | |
| SAVE_FPS = 25 | |
| NUM_FRAMES = 125 | |
| VIDEO_SECONDS = NUM_FRAMES / SAVE_FPS | |
| AUDIO_STRIDE = 1 | |
| CP_SPLIT_HW = [1, 1] | |
| AUDIO_GUIDANCE_SCALE = 2.0 | |
| NEGATIVE_PROMPT = ( | |
| "Close-up, Bright tones, overexposed, static, blurred details, subtitles, " | |
| "style, works, paintings, images, static, overall gray, worst quality, low " | |
| "quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly " | |
| "drawn hands, poorly drawn faces, deformed, disfigured, misshapen limbs, " | |
| "fused fingers, still picture, messy background, three legs, many people in " | |
| "the background, walking backwards" | |
| ) | |
| ACCEL_MODE_EXACT = "Exact 8-step" | |
| ACCEL_MODE_DBCACHE = "DBCache fast" | |
| ACCEL_MODE_DBCACHE_FASTER = "DBCache faster" | |
| EXAMPLE_CACHE_VERSION = "v4" | |
| IMAGE_ERROR_MESSAGE = "Could not open the uploaded image. Please upload a valid image file (PNG, JPG, WEBP, etc.)" | |
| _AUDIO_EMB_CACHE = OrderedDict() | |
| _CACHE_LIMIT = 8 | |
| _DISK_CACHE_DIR = Path(tempfile.gettempdir()) / "avatar_demo_cache" | |
| _AUDIO_CACHE_DIR = _DISK_CACHE_DIR / "audio_emb" | |
| _EXAMPLE_CACHE_DIR = _DISK_CACHE_DIR / "examples" | |
| _AUDIO_CACHE_DIR.mkdir(parents=True, exist_ok=True) | |
| _EXAMPLE_CACHE_DIR.mkdir(parents=True, exist_ok=True) | |
| def _install_sdpa_shim() -> None: | |
| """Patch xformers-style calls to PyTorch SDPA. | |
| A local pure-PyTorch xformers shim is bundled with this Space, so this | |
| function works whether or not the external xformers package is installed. | |
| """ | |
| import xformers.ops | |
| class _BDShim: | |
| def __init__(self, q_seqlen, kv_seqlen): | |
| self.q_seqlen = list(q_seqlen) | |
| self.kv_seqlen = list(kv_seqlen) | |
| def from_seqlens(cls, q_seqlen, kv_seqlen): | |
| return cls(q_seqlen, kv_seqlen) | |
| xformers.ops.fmha.attn_bias.BlockDiagonalMask = _BDShim | |
| def _meff(q, k, v, attn_bias=None, op=None, **_): | |
| if attn_bias is None: | |
| q_ = q.transpose(1, 2).contiguous() | |
| k_ = k.transpose(1, 2).contiguous() | |
| v_ = v.transpose(1, 2).contiguous() | |
| return F.scaled_dot_product_attention(q_, k_, v_).transpose(1, 2) | |
| if isinstance(attn_bias, _BDShim): | |
| outs, q_off, k_off = [], 0, 0 | |
| for q_len, k_len in zip(attn_bias.q_seqlen, attn_bias.kv_seqlen): | |
| q_b = q[:, q_off : q_off + q_len].transpose(1, 2).contiguous() | |
| k_b = k[:, k_off : k_off + k_len].transpose(1, 2).contiguous() | |
| v_b = v[:, k_off : k_off + k_len].transpose(1, 2).contiguous() | |
| outs.append(F.scaled_dot_product_attention(q_b, k_b, v_b).transpose(1, 2)) | |
| q_off += q_len | |
| k_off += k_len | |
| return torch.cat(outs, dim=1) | |
| raise NotImplementedError(f"Unsupported attn_bias in SDPA shim: {type(attn_bias)}") | |
| xformers.ops.memory_efficient_attention = _meff | |
| print("[boot] installed xformers->SDPA shim", flush=True) | |
| def _runtime_device() -> str: | |
| if os.environ.get("SPACES_ZERO_GPU"): | |
| return "cuda" | |
| return "cuda" if torch.cuda.is_available() else "cpu" | |
| def _download_longcat_weights() -> None: | |
| WEIGHTS_DIR.mkdir(parents=True, exist_ok=True) | |
| print(f"[boot] WEIGHTS_DIR={WEIGHTS_DIR.resolve()}", flush=True) | |
| if not (BASE_DIR / "vae" / "config.json").exists(): | |
| print("[boot] downloading LongCat-Video VAE/text encoder/tokenizer", flush=True) | |
| snapshot_download( | |
| "meituan-longcat/LongCat-Video", | |
| local_dir=str(BASE_DIR), | |
| token=HF_TOKEN, | |
| allow_patterns=[ | |
| "tokenizer/*", | |
| "text_encoder/*.safetensors", | |
| "text_encoder/*.json", | |
| "vae/*.safetensors", | |
| "vae/*.json", | |
| ], | |
| ignore_patterns=[ | |
| "text_encoder/*.fp32*", | |
| "text_encoder/*.bin", | |
| "text_encoder/flax_model*", | |
| "text_encoder/tf_model*", | |
| "vae/flax_model*", | |
| "vae/tf_model*", | |
| ], | |
| ) | |
| if not (AVATAR_DIR / "base_model_int8" / "config.json").exists(): | |
| print("[boot] downloading LongCat-Video-Avatar 1.5", flush=True) | |
| snapshot_download( | |
| "meituan-longcat/LongCat-Video-Avatar-1.5", | |
| local_dir=str(AVATAR_DIR), | |
| token=HF_TOKEN, | |
| allow_patterns=[ | |
| "base_model_int8/*", | |
| "lora/*", | |
| "scheduler/*", | |
| "vocal_separator/*", | |
| "whisper-large-v3/model.safetensors", | |
| "whisper-large-v3/*.json", | |
| "whisper-large-v3/*.txt", | |
| ], | |
| ignore_patterns=[ | |
| "whisper-large-v3/model.fp32*", | |
| "whisper-large-v3/flax_model*", | |
| "whisper-large-v3/tf_model*", | |
| "whisper-large-v3/pytorch_model*", | |
| ], | |
| ) | |
| print("[boot] LongCat weights ready", flush=True) | |
| def _patch_dit_config() -> None: | |
| cfg_path = AVATAR_DIR / "base_model_int8" / "config.json" | |
| if not cfg_path.exists(): | |
| return | |
| cfg = json.loads(cfg_path.read_text()) | |
| changed = False | |
| for key in ("enable_flashattn2", "enable_flashattn3", "enable_bsa"): | |
| if cfg.get(key): | |
| cfg[key] = False | |
| changed = True | |
| if not cfg.get("enable_xformers"): | |
| cfg["enable_xformers"] = True | |
| changed = True | |
| if changed: | |
| cfg_path.write_text(json.dumps(cfg, indent=2)) | |
| print("[boot] patched DiT config -> SDPA backend", flush=True) | |
| def _file_sha256(path: str) -> str: | |
| h = hashlib.sha256() | |
| with open(path, "rb") as f: | |
| for chunk in iter(lambda: f.read(1024 * 1024), b""): | |
| h.update(chunk) | |
| return h.hexdigest() | |
| def _cache_get(cache: OrderedDict, key): | |
| value = cache.get(key) | |
| if value is not None: | |
| cache.move_to_end(key) | |
| return value | |
| def _cache_put(cache: OrderedDict, key, value) -> None: | |
| cache[key] = value | |
| cache.move_to_end(key) | |
| while len(cache) > _CACHE_LIMIT: | |
| cache.popitem(last=False) | |
| def _cache_file(namespace: Path, key) -> Path: | |
| key_json = json.dumps(key, sort_keys=True, separators=(",", ":")) | |
| return namespace / f"{hashlib.sha256(key_json.encode('utf-8')).hexdigest()}.pt" | |
| def _load_audio_16k(path: str): | |
| try: | |
| from scipy.signal import resample_poly | |
| speech, sr = sf.read(path, dtype="float32", always_2d=False) | |
| if speech.ndim > 1: | |
| speech = speech.mean(axis=1) | |
| if sr != 16000: | |
| gcd = math.gcd(int(sr), 16000) | |
| speech = resample_poly(speech, 16000 // gcd, int(sr) // gcd).astype(np.float32) | |
| sr = 16000 | |
| return np.ascontiguousarray(speech, dtype=np.float32), sr | |
| except Exception as exc: | |
| print(f"[audio] soundfile load failed, falling back to librosa: {exc}", flush=True) | |
| import librosa | |
| speech, sr = librosa.load(path, sr=16000) | |
| return np.ascontiguousarray(speech, dtype=np.float32), sr | |
| def _prepare_audio_embedding(audio_path: str, progress): | |
| audio_hash = _file_sha256(audio_path) | |
| cache_key = (audio_hash, NUM_FRAMES, SAVE_FPS, AUDIO_STRIDE) | |
| cached = _cache_get(_AUDIO_EMB_CACHE, cache_key) | |
| if cached is not None: | |
| progress(0.26, desc="Using cached audio conditioning") | |
| print(f"[cache] audio embedding hit {audio_hash[:10]}", flush=True) | |
| return cached.to(DEVICE, non_blocking=True) | |
| cache_path = _cache_file(_AUDIO_CACHE_DIR, cache_key) | |
| if cache_path.exists(): | |
| try: | |
| cached = torch.load(cache_path, map_location="cpu") | |
| _cache_put(_AUDIO_EMB_CACHE, cache_key, cached) | |
| progress(0.26, desc="Using cached audio conditioning") | |
| print(f"[cache] audio embedding disk hit {audio_hash[:10]}", flush=True) | |
| return cached.to(DEVICE, non_blocking=True) | |
| except Exception as exc: | |
| print(f"[cache] audio disk cache read failed: {exc}", flush=True) | |
| t0 = time.perf_counter() | |
| speech, sr = _load_audio_16k(audio_path) | |
| pad = math.ceil((NUM_FRAMES / SAVE_FPS - len(speech) / sr) * sr) | |
| if pad > 0: | |
| speech = np.concatenate([speech, np.zeros(pad, dtype=speech.dtype)]) | |
| print(f"[timing] audio_load={time.perf_counter() - t0:.2f}s sr={sr} samples={len(speech)}", flush=True) | |
| progress(0.30, desc="Encoding audio") | |
| t0 = time.perf_counter() | |
| full_audio_emb = longcat_pipe.get_audio_embedding( | |
| speech, | |
| fps=SAVE_FPS * AUDIO_STRIDE, | |
| device=DEVICE, | |
| sample_rate=sr, | |
| model_type="avatar-v1.5", | |
| ) | |
| if torch.isnan(full_audio_emb).any(): | |
| raise gr.Error("Audio embedding contains NaN. Try shorter text or another voice.") | |
| indices = torch.arange(2 * 2 + 1, device=full_audio_emb.device) - 2 | |
| center = ( | |
| torch.arange(0, AUDIO_STRIDE * NUM_FRAMES, AUDIO_STRIDE, device=full_audio_emb.device).unsqueeze(1) | |
| + indices.unsqueeze(0) | |
| ) | |
| center = torch.clamp(center, min=0, max=full_audio_emb.shape[0] - 1) | |
| audio_emb = full_audio_emb[center][None, ...].to(DEVICE) | |
| print(f"[timing] audio_encode={time.perf_counter() - t0:.2f}s shape={tuple(audio_emb.shape)}", flush=True) | |
| audio_emb_cpu = audio_emb.detach().cpu() | |
| _cache_put(_AUDIO_EMB_CACHE, cache_key, audio_emb_cpu) | |
| try: | |
| torch.save(audio_emb_cpu, cache_path) | |
| except Exception as exc: | |
| print(f"[cache] audio disk cache write failed: {exc}", flush=True) | |
| return audio_emb | |
| def _fit_audio_to_video_duration(audio_path: str, duration: float = VIDEO_SECONDS) -> str: | |
| waveform, sample_rate = sf.read(audio_path, dtype="float32", always_2d=False) | |
| target_samples = int(round(sample_rate * duration)) | |
| if waveform.ndim == 1: | |
| current_samples = waveform.shape[0] | |
| if current_samples < target_samples: | |
| waveform = np.pad(waveform, (0, target_samples - current_samples)) | |
| else: | |
| waveform = waveform[:target_samples] | |
| else: | |
| current_samples = waveform.shape[0] | |
| if current_samples < target_samples: | |
| waveform = np.pad(waveform, ((0, target_samples - current_samples), (0, 0))) | |
| else: | |
| waveform = waveform[:target_samples, :] | |
| fitted_path = Path(tempfile.gettempdir()) / f"longcat_drive_{uuid.uuid4().hex[:10]}.wav" | |
| sf.write(str(fitted_path), waveform, sample_rate) | |
| print( | |
| "[audio] video driving audio " | |
| f"input={current_samples / sample_rate:.3f}s output={len(waveform) / sample_rate:.3f}s " | |
| f"sr={sample_rate} path={fitted_path}", | |
| flush=True, | |
| ) | |
| return str(fitted_path) | |
| def _normalise_video_frames(frames: np.ndarray) -> np.ndarray: | |
| frames = np.asarray(frames) | |
| frame_min = float(np.nanmin(frames)) | |
| frame_max = float(np.nanmax(frames)) | |
| print( | |
| f"[video] raw_frames shape={frames.shape} dtype={frames.dtype} min={frame_min:.4f} max={frame_max:.4f}", | |
| flush=True, | |
| ) | |
| if np.issubdtype(frames.dtype, np.floating) and frame_max <= 1.5: | |
| frames = frames * 255.0 | |
| frames = np.nan_to_num(frames, nan=0.0, posinf=255.0, neginf=0.0) | |
| frames = np.clip(frames, 0, 255).astype(np.uint8) | |
| print( | |
| f"[video] encoded_frames count={len(frames)} duration={len(frames) / SAVE_FPS:.3f}s " | |
| f"dtype={frames.dtype}", | |
| flush=True, | |
| ) | |
| return frames | |
| def _save_video_ffmpeg_fast(frames: np.ndarray, out_base: Path, audio_path: str, fps: int, quality: int = 5) -> str: | |
| out_base = str(out_base) | |
| temp_video = out_base + "-video.mp4" | |
| out_path = out_base + ".mp4" | |
| writer = imageio.get_writer(temp_video, fps=fps, codec="libx264", quality=quality) | |
| try: | |
| for frame in frames: | |
| writer.append_data(np.asarray(frame)) | |
| finally: | |
| writer.close() | |
| duration = len(frames) / fps | |
| cmd = [ | |
| "ffmpeg", | |
| "-y", | |
| "-loglevel", | |
| "error", | |
| "-i", | |
| temp_video, | |
| "-i", | |
| audio_path, | |
| "-t", | |
| f"{duration:.3f}", | |
| "-map", | |
| "0:v:0", | |
| "-map", | |
| "1:a:0", | |
| "-c:v", | |
| "copy", | |
| "-c:a", | |
| "aac", | |
| "-b:a", | |
| "96k", | |
| "-movflags", | |
| "+faststart", | |
| out_path, | |
| ] | |
| subprocess.run(cmd, check=True) | |
| try: | |
| os.remove(temp_video) | |
| except OSError: | |
| pass | |
| return out_path | |
| def _configure_dit_acceleration(acceleration: str) -> str: | |
| if acceleration in (ACCEL_MODE_DBCACHE, ACCEL_MODE_DBCACHE_FASTER): | |
| faster = acceleration == ACCEL_MODE_DBCACHE_FASTER | |
| longcat_pipe.dit.configure_dbcache( | |
| enabled=True, | |
| fn=1, | |
| bn=0, | |
| warmup_steps=1, | |
| max_cached_steps=3 if faster else 2, | |
| max_continuous_cached_steps=1, | |
| residual_diff_threshold=0.35, | |
| downsample_factor=4, | |
| ) | |
| return "DMD2 8-step + DBCache" + (" faster" if faster else "") | |
| longcat_pipe.dit.configure_dbcache(enabled=False) | |
| return "DMD2 8-step" | |
| def _write_wav(sample_rate: int, waveform: np.ndarray) -> str: | |
| audio_path = Path(tempfile.gettempdir()) / f"gepard_{uuid.uuid4().hex[:10]}.wav" | |
| sf.write(str(audio_path), waveform, sample_rate) | |
| return str(audio_path) | |
| def _coerce_file_path(file_value) -> str | None: | |
| if not file_value: | |
| return None | |
| if isinstance(file_value, dict): | |
| return file_value.get("path") or file_value.get("name") | |
| if isinstance(file_value, (str, os.PathLike)): | |
| return str(file_value) | |
| name = getattr(file_value, "name", None) | |
| return str(name) if name else None | |
| def _open_reference_image(image_value): | |
| image_path = _coerce_file_path(image_value) | |
| if not image_path: | |
| raise gr.Error("Upload a reference image.") | |
| try: | |
| return Image.open(image_path).convert("RGB"), image_path | |
| except (UnidentifiedImageError, OSError, ValueError) as exc: | |
| raise gr.Error(IMAGE_ERROR_MESSAGE) from exc | |
| def preview_reference_image(image_value): | |
| if not image_value: | |
| return None | |
| try: | |
| _, image_path = _open_reference_image(image_value) | |
| return image_path | |
| except gr.Error: | |
| gr.Warning(IMAGE_ERROR_MESSAGE) | |
| return None | |
| def _is_valid_image_file(path: Path) -> bool: | |
| try: | |
| with Image.open(path) as image: | |
| image.verify() | |
| return True | |
| except Exception: | |
| return False | |
| def _resolve_example_image() -> Path | None: | |
| local_path = ROOT / "assets" / "avatar" / "single" / "character.png" | |
| if local_path.exists() and _is_valid_image_file(local_path): | |
| return local_path | |
| try: | |
| downloaded = hf_hub_download( | |
| "Mike0021/avatar-demo", | |
| "assets/avatar/single/character.png", | |
| repo_type="space", | |
| token=HF_TOKEN, | |
| ) | |
| downloaded_path = Path(downloaded) | |
| if _is_valid_image_file(downloaded_path): | |
| return downloaded_path | |
| except Exception as exc: | |
| print(f"[examples] failed to resolve example image: {exc}", flush=True) | |
| return None | |
| def _estimate_duration(*args, **kwargs) -> int: | |
| return 240 | |
| _install_sdpa_shim() | |
| from transformers import AutoTokenizer, UMT5EncoderModel # noqa: E402 | |
| from longcat_video.audio_process import ( # noqa: E402 | |
| get_audio_encoder, | |
| get_audio_feature_extractor, | |
| ) | |
| from longcat_video.modules.autoencoder_kl_wan import AutoencoderKLWan # noqa: E402 | |
| from longcat_video.modules.quantization import load_quantized_dit # noqa: E402 | |
| from longcat_video.modules.scheduling_flow_match_euler_discrete import ( # noqa: E402 | |
| FlowMatchEulerDiscreteScheduler, | |
| ) | |
| from longcat_video.pipeline_longcat_video_avatar import LongCatVideoAvatarPipeline # noqa: E402 | |
| if torch.cuda.is_available(): | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| try: | |
| torch.set_float32_matmul_precision("high") | |
| except Exception: | |
| pass | |
| print("[boot] loading GEPARD", flush=True) | |
| gepard_config = AppConfig.from_yaml(CONFIG_PATH) | |
| gepard_engine = GepardEngine(gepard_config).load() | |
| print("[boot] GEPARD ready", flush=True) | |
| _download_longcat_weights() | |
| _patch_dit_config() | |
| DEVICE = _runtime_device() | |
| TORCH_DTYPE = torch.bfloat16 if DEVICE == "cuda" else torch.float32 | |
| print(f"[boot] LongCat device={DEVICE} dtype={TORCH_DTYPE}", flush=True) | |
| print("[boot] LongCat tokenizer + text_encoder", flush=True) | |
| _t = time.time() | |
| tokenizer = AutoTokenizer.from_pretrained(str(BASE_DIR), subfolder="tokenizer", torch_dtype=TORCH_DTYPE) | |
| text_encoder = UMT5EncoderModel.from_pretrained(str(BASE_DIR), subfolder="text_encoder", torch_dtype=TORCH_DTYPE) | |
| print(f"[boot] text_encoder loaded in {time.time() - _t:.1f}s", flush=True) | |
| print("[boot] LongCat VAE + scheduler", flush=True) | |
| _t = time.time() | |
| vae = AutoencoderKLWan.from_pretrained(str(BASE_DIR), subfolder="vae", torch_dtype=TORCH_DTYPE) | |
| scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(str(AVATAR_DIR), subfolder="scheduler", torch_dtype=TORCH_DTYPE) | |
| print(f"[boot] VAE+scheduler loaded in {time.time() - _t:.1f}s", flush=True) | |
| print("[boot] LongCat INT8 DiT + DMD2 LoRA", flush=True) | |
| _t = time.time() | |
| dit = load_quantized_dit(str(AVATAR_DIR), subfolder="base_model_int8", cp_split_hw=CP_SPLIT_HW) | |
| lora_path = AVATAR_DIR / "lora" / "dmd_lora.safetensors" | |
| if lora_path.exists(): | |
| dit.load_lora(str(lora_path), "dmd", multiplier=1.0, lora_network_dim=128, lora_network_alpha=64) | |
| dit.enable_loras(["dmd"]) | |
| print("[boot] DMD2 8-step LoRA enabled", flush=True) | |
| print(f"[boot] DiT loaded in {time.time() - _t:.1f}s", flush=True) | |
| print("[boot] LongCat Whisper-Large-v3", flush=True) | |
| _t = time.time() | |
| audio_encoder = get_audio_encoder(str(AVATAR_DIR / "whisper-large-v3"), "avatar-v1.5") | |
| audio_feature_extractor = get_audio_feature_extractor(str(AVATAR_DIR / "whisper-large-v3"), "avatar-v1.5") | |
| print(f"[boot] Whisper loaded in {time.time() - _t:.1f}s", flush=True) | |
| print("[boot] assembling LongCat pipeline", flush=True) | |
| longcat_pipe = LongCatVideoAvatarPipeline( | |
| tokenizer=tokenizer, | |
| text_encoder=text_encoder, | |
| vae=vae, | |
| scheduler=scheduler, | |
| dit=dit, | |
| audio_encoder=audio_encoder, | |
| audio_feature_extractor=audio_feature_extractor, | |
| model_type="avatar-v1.5", | |
| ) | |
| longcat_pipe.to(DEVICE) | |
| audio_encoder.to(DEVICE, dtype=TORCH_DTYPE) | |
| print("[boot] LongCat ready", flush=True) | |
| def _zerogpu_probe(): | |
| return "ready" | |
| def _generate_talking_avatar_impl( | |
| text: str, | |
| image_path: str, | |
| voice_mode: str, | |
| speaker: str, | |
| reference_audio: str, | |
| prompt: str, | |
| resolution: str, | |
| seed: int, | |
| temperature: float, | |
| max_speech_frames: int, | |
| repetition_penalty: float, | |
| repetition_window: int, | |
| acceleration: str, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| if not (text or "").strip(): | |
| raise gr.Error("Enter text to synthesize.") | |
| image, image_path = _open_reference_image(image_path) | |
| reference_audio = _coerce_file_path(reference_audio) | |
| progress(0.02, desc="Preparing voice") | |
| if voice_mode == MODE_PRESET: | |
| if not speaker: | |
| raise gr.Error("Choose a preset speaker.") | |
| ref_codes = gepard_engine.speakers.codes(speaker) | |
| else: | |
| if not reference_audio: | |
| raise gr.Error("Upload a reference audio clip for voice cloning.") | |
| try: | |
| ref_codes = gepard_engine.encode_reference(reference_audio) | |
| except Exception as exc: | |
| raise gr.Error( | |
| "Could not process the reference audio. Please upload a clear WAV, MP3, or M4A voice clip." | |
| ) from exc | |
| params = GenerationParams( | |
| temperature=temperature, | |
| top_k=gepard_config.defaults.top_k, | |
| cfg_scale=gepard_config.defaults.cfg_scale, | |
| cfg_frames=gepard_config.defaults.cfg_frames, | |
| stop_threshold=gepard_config.defaults.stop_threshold, | |
| max_frames=int(max_speech_frames), | |
| repetition_penalty=float(repetition_penalty), | |
| repetition_window=int(repetition_window), | |
| ) | |
| progress(0.08, desc="Synthesizing speech") | |
| t_total = time.perf_counter() | |
| t0 = time.perf_counter() | |
| sample_rate, waveform = gepard_engine.synthesize(text, ref_codes, params) | |
| audio_path = _write_wav(sample_rate, waveform) | |
| print(f"[timing] tts={time.perf_counter() - t0:.2f}s sr={sample_rate} path={audio_path}", flush=True) | |
| video_audio_path = _fit_audio_to_video_duration(audio_path) | |
| audio_emb = _prepare_audio_embedding(video_audio_path, progress) | |
| generation_mode = _configure_dit_acceleration(acceleration) | |
| progress(0.40, desc=f"Generating video ({generation_mode})") | |
| generator = torch.Generator(device=DEVICE).manual_seed(int(seed)) | |
| clean_prompt = (prompt or default_prompt).strip() | |
| t0 = time.perf_counter() | |
| with torch.inference_mode(): | |
| output = longcat_pipe.generate_ai2v( | |
| image=image, | |
| prompt=clean_prompt, | |
| negative_prompt=NEGATIVE_PROMPT, | |
| resolution=resolution, | |
| num_frames=NUM_FRAMES, | |
| num_inference_steps=8, | |
| text_guidance_scale=1.0, | |
| audio_guidance_scale=AUDIO_GUIDANCE_SCALE, | |
| output_type="np", | |
| generator=generator, | |
| audio_emb=audio_emb, | |
| use_distill=True, | |
| ) | |
| print(f"[timing] video_generate={time.perf_counter() - t0:.2f}s mode={acceleration}", flush=True) | |
| if acceleration in (ACCEL_MODE_DBCACHE, ACCEL_MODE_DBCACHE_FASTER): | |
| print(f"[dbcache] {longcat_pipe.dit.get_dbcache_stats()}", flush=True) | |
| progress(0.92, desc="Muxing audio and video") | |
| t0 = time.perf_counter() | |
| frames = _normalise_video_frames(output[0]) | |
| out_base = Path(tempfile.gettempdir()) / f"avatar_{uuid.uuid4().hex[:10]}" | |
| video_path = _save_video_ffmpeg_fast(frames, out_base, video_audio_path, fps=SAVE_FPS, quality=5) | |
| print(f"[timing] mux={time.perf_counter() - t0:.2f}s total={time.perf_counter() - t_total:.2f}s", flush=True) | |
| print(f"[gen] audio={audio_path} video={video_path}", flush=True) | |
| return audio_path, video_path | |
| def _generate_talking_avatar_gpu( | |
| text: str, | |
| image_path: str, | |
| voice_mode: str, | |
| speaker: str, | |
| reference_audio: str, | |
| prompt: str, | |
| resolution: str, | |
| seed: int, | |
| temperature: float, | |
| max_speech_frames: int, | |
| repetition_penalty: float, | |
| repetition_window: int, | |
| acceleration: str, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| return _generate_talking_avatar_impl( | |
| text, | |
| image_path, | |
| voice_mode, | |
| speaker, | |
| reference_audio, | |
| prompt, | |
| resolution, | |
| seed, | |
| temperature, | |
| max_speech_frames, | |
| repetition_penalty, | |
| repetition_window, | |
| acceleration, | |
| progress, | |
| ) | |
| def generate_talking_avatar( | |
| text: str, | |
| image_path: str, | |
| voice_mode: str, | |
| speaker: str, | |
| reference_audio: str, | |
| prompt: str, | |
| resolution: str, | |
| seed: int, | |
| temperature: float, | |
| max_speech_frames: int, | |
| repetition_penalty: float, | |
| repetition_window: int, | |
| acceleration: str, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| if not (text or "").strip(): | |
| gr.Warning("Enter text to synthesize.") | |
| return None, None | |
| if not _coerce_file_path(image_path): | |
| gr.Warning("Upload a reference image.") | |
| return None, None | |
| try: | |
| with Image.open(_coerce_file_path(image_path)) as image: | |
| image.verify() | |
| except (UnidentifiedImageError, OSError, ValueError): | |
| gr.Warning(IMAGE_ERROR_MESSAGE) | |
| return None, None | |
| if voice_mode == MODE_CLONE and not _coerce_file_path(reference_audio): | |
| gr.Warning("Upload a reference audio clip for voice cloning.") | |
| return None, None | |
| return _generate_talking_avatar_gpu( | |
| text, | |
| image_path, | |
| voice_mode, | |
| speaker, | |
| reference_audio, | |
| prompt, | |
| resolution, | |
| seed, | |
| temperature, | |
| max_speech_frames, | |
| repetition_penalty, | |
| repetition_window, | |
| acceleration, | |
| progress, | |
| ) | |
| def _toggle_voice_mode(mode: str): | |
| return ( | |
| gr.update(visible=mode == MODE_PRESET), | |
| gr.update(visible=mode == MODE_CLONE), | |
| ) | |
| CUSTOM_CSS = """ | |
| :root { | |
| --app-radius: 14px; | |
| --app-card-pad: 18px; | |
| } | |
| .gradio-container, | |
| .fillable:not(.fill_width) { | |
| width: min(100%, 1280px) !important; | |
| max-width: 1280px !important; | |
| margin-left: auto !important; | |
| margin-right: auto !important; | |
| } | |
| #generate-btn { | |
| font-weight: 700; | |
| font-size: 1.02rem; | |
| padding: 0.7rem 1rem; | |
| } | |
| /* Hero header */ | |
| .app-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| padding: 4px 0 16px; | |
| border-bottom: 1px solid var(--border-color-primary); | |
| margin-bottom: 18px; | |
| } | |
| .app-header-accent { | |
| width: 8px; | |
| height: 46px; | |
| border-radius: 999px; | |
| background: linear-gradient(180deg, #f59e0b, #ea580c 55%, #dc2626); | |
| flex: 0 0 auto; | |
| } | |
| .app-header h1 { | |
| margin: 0; | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| line-height: 1.15; | |
| background: linear-gradient(90deg, #f59e0b, #ea580c 60%, #dc2626); | |
| -webkit-background-clip: text; | |
| background-clip: text; | |
| color: transparent; | |
| } | |
| .app-header p { | |
| margin: 4px 0 0; | |
| color: var(--body-text-color-subdued); | |
| font-size: 0.95rem; | |
| line-height: 1.4; | |
| } | |
| /* Panel cards for the two-column layout */ | |
| .panel { | |
| background: var(--background-fill-primary); | |
| border: 1px solid var(--border-color-primary); | |
| border-radius: var(--app-radius); | |
| padding: var(--app-card-pad); | |
| height: 100%; | |
| } | |
| .panel-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 9px; | |
| font-weight: 700; | |
| font-size: 1.02rem; | |
| margin: 0 0 12px; | |
| color: var(--primary_600); | |
| } | |
| .panel-title.muted { color: var(--body-text-color-subdued); font-weight: 600; } | |
| .panel-title .step { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #f59e0b, #ea580c); | |
| color: white; | |
| font-size: 0.78rem; | |
| font-weight: 700; | |
| flex: 0 0 auto; | |
| } | |
| .section-label { | |
| font-size: 0.78rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.06em; | |
| color: var(--primary_600); | |
| margin: 14px 0 4px; | |
| } | |
| .section-label:first-child { margin-top: 0; } | |
| /* Two-step pipeline indicator on the result panel */ | |
| .pipeline-steps { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin: 0 0 12px; | |
| } | |
| .pipeline-step { | |
| flex: 1 1 160px; | |
| border: 1px solid var(--border-color-primary); | |
| border-radius: 12px; | |
| padding: 10px 12px; | |
| background: var(--background-fill-secondary); | |
| } | |
| .pipeline-step .ps-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.85rem; | |
| font-weight: 700; | |
| color: var(--primary_600); | |
| } | |
| .pipeline-step .ps-title .ps-num { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 18px; | |
| height: 18px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, #f59e0b, #ea580c); | |
| color: #fff; | |
| font-size: 0.68rem; | |
| } | |
| .pipeline-step .ps-desc { | |
| margin: 4px 0 0; | |
| font-size: 0.8rem; | |
| color: var(--body-text-color-subdued); | |
| line-height: 1.35; | |
| } | |
| .status-box { | |
| border: 1px dashed var(--border-color-primary); | |
| border-radius: 10px; | |
| padding: 10px 14px; | |
| background: var(--background-fill-secondary); | |
| min-height: 22px; | |
| line-height: 1.45; | |
| font-size: 0.9rem; | |
| color: var(--body-text-color-subdued); | |
| margin: 0 0 14px; | |
| } | |
| .result-video { | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| /* Examples */ | |
| .example-section { | |
| border-top: 1px solid var(--border-color-primary); | |
| margin-top: 24px; | |
| padding-top: 18px; | |
| overflow-x: hidden; | |
| } | |
| .example-section h3 { margin: 0 0 12px; font-size: 1.08rem; } | |
| .example-card { | |
| border: 1px solid var(--border-color-primary); | |
| border-radius: var(--app-radius); | |
| padding: 14px; | |
| display: flex; | |
| gap: 14px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .example-card img { object-fit: cover; border-radius: 10px; } | |
| .footer-note { | |
| text-align: center; | |
| color: var(--body-text-color-subdued); | |
| font-size: 0.85rem; | |
| margin-top: 20px; | |
| } | |
| #reference-image-upload, | |
| #reference-image-preview { | |
| min-height: 160px !important; | |
| } | |
| /* Responsive: stack panels on narrow screens */ | |
| .app-row { flex-wrap: wrap; } | |
| @media (max-width: 820px) { | |
| .app-row > div { flex-basis: 100% !important; min-width: 100% !important; } | |
| .app-header-accent { height: 40px; } | |
| .app-header h1 { font-size: 1.3rem; } | |
| .panel { padding: 14px; } | |
| .pipeline-step { flex: 1 1 100%; } | |
| #reference-image-upload, | |
| #reference-image-preview { | |
| min-height: 140px !important; | |
| } | |
| } | |
| """ | |
| speaker_choices = [ | |
| (gepard_config.speaker_labels.get(name, name), name) | |
| for name in gepard_engine.speakers.names | |
| ] | |
| default_prompt = "A person talks directly to the camera with natural facial expressions and small head movements." | |
| example_image = _resolve_example_image() | |
| example_presets = list(gepard_config.examples[:6]) if example_image else [] | |
| example_choices = [ | |
| ( | |
| f"{idx + 1}. {gepard_config.speaker_labels.get(ex.speaker, ex.speaker)} - {ex.text[:78]}", | |
| str(idx), | |
| ) | |
| for idx, ex in enumerate(example_presets) | |
| ] | |
| if example_image: | |
| print(f"[examples] using image {example_image}", flush=True) | |
| else: | |
| print("[examples] no valid example image found; examples hidden", flush=True) | |
| def _example_cache_paths(index: int) -> tuple[Path, Path]: | |
| stem = f"{EXAMPLE_CACHE_VERSION}_{index}" | |
| return _EXAMPLE_CACHE_DIR / f"{stem}.wav", _EXAMPLE_CACHE_DIR / f"{stem}.mp4" | |
| def _packaged_example_paths(index: int) -> tuple[Path, Path] | None: | |
| stem = f"{EXAMPLE_CACHE_VERSION}_{index}" | |
| packaged_dir = ROOT / "assets" / "avatar" / "cached_examples" | |
| audio_path = packaged_dir / f"{stem}.wav" | |
| video_path = packaged_dir / f"{stem}.mp4" | |
| if audio_path.exists() and video_path.exists(): | |
| return audio_path, video_path | |
| return None | |
| def _example_outputs(index: int, audio_path: Path, video_path: Path) -> list: | |
| ex = example_presets[index] | |
| image_path = str(example_image) | |
| return [ | |
| ex.text, | |
| image_path, | |
| image_path, | |
| MODE_PRESET, | |
| ex.speaker, | |
| None, | |
| default_prompt, | |
| "480p", | |
| 42, | |
| gepard_config.defaults.temperature, | |
| 215, | |
| gepard_config.defaults.repetition_penalty, | |
| gepard_config.defaults.repetition_window, | |
| ACCEL_MODE_EXACT, | |
| str(audio_path), | |
| str(video_path), | |
| "✅ Cached example loaded. Listen to the speech, then watch the video — or tweak the inputs and press Generate.", | |
| ] | |
| def _parse_example_index(example_index: str) -> int: | |
| if not example_presets: | |
| raise gr.Error("No valid examples are available.") | |
| try: | |
| index = int(example_index) | |
| except (TypeError, ValueError) as exc: | |
| raise gr.Error("Choose an example preset.") from exc | |
| if index < 0 or index >= len(example_presets): | |
| raise gr.Error("Choose an example preset.") | |
| return index | |
| def _generate_cached_example_gpu(index: int, progress=gr.Progress(track_tqdm=True)): | |
| ex = example_presets[index] | |
| print(f"[examples] cache miss index={index} speaker={ex.speaker}", flush=True) | |
| audio_path, video_path = _generate_talking_avatar_impl( | |
| ex.text, | |
| str(example_image), | |
| MODE_PRESET, | |
| ex.speaker, | |
| None, | |
| default_prompt, | |
| "480p", | |
| 42, | |
| gepard_config.defaults.temperature, | |
| 215, | |
| gepard_config.defaults.repetition_penalty, | |
| gepard_config.defaults.repetition_window, | |
| ACCEL_MODE_EXACT, | |
| progress, | |
| ) | |
| audio_cache, video_cache = _example_cache_paths(index) | |
| shutil.copyfile(audio_path, audio_cache) | |
| shutil.copyfile(video_path, video_cache) | |
| return _example_outputs(index, audio_cache, video_cache) | |
| def run_cached_example(example_index: str, progress=gr.Progress(track_tqdm=True)): | |
| index = _parse_example_index(example_index) | |
| audio_cache, video_cache = _example_cache_paths(index) | |
| if audio_cache.exists() and video_cache.exists(): | |
| progress(1.0, desc="Using cached example") | |
| print(f"[examples] cache hit index={index}", flush=True) | |
| return _example_outputs(index, audio_cache, video_cache) | |
| packaged_paths = _packaged_example_paths(index) | |
| if packaged_paths is not None: | |
| progress(1.0, desc="Using packaged cached example") | |
| print(f"[examples] packaged cache hit index={index}", flush=True) | |
| return _example_outputs(index, *packaged_paths) | |
| return _generate_cached_example_gpu(index, progress) | |
| def run_generate( | |
| text: str, | |
| image_path: str, | |
| voice_mode: str, | |
| speaker: str, | |
| reference_audio: str, | |
| prompt: str, | |
| resolution: str, | |
| seed: int, | |
| temperature: float, | |
| max_speech_frames: int, | |
| repetition_penalty: float, | |
| repetition_window: int, | |
| acceleration: str, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| """UI-level wrapper that turns the (audio, video) backend result into a | |
| (status, audio, video) triple for the redesigned output panel. | |
| No inference happens here — the ZeroGPU-decorated ``generate_talking_avatar`` | |
| still owns all GPU work. This only formats the result for presentation. | |
| """ | |
| audio, video = generate_talking_avatar( | |
| text, | |
| image_path, | |
| voice_mode, | |
| speaker, | |
| reference_audio, | |
| prompt, | |
| resolution, | |
| seed, | |
| temperature, | |
| max_speech_frames, | |
| repetition_penalty, | |
| repetition_window, | |
| acceleration, | |
| progress, | |
| ) | |
| if audio and video: | |
| status = "✅ Done! Play the speech preview above, then watch your talking avatar video below." | |
| classes = ["status-box", "status-done"] | |
| else: | |
| status = "⚠️ Generation didn't finish. Complete the inputs on the left, then press Generate." | |
| classes = ["status-box", "status-idle"] | |
| return gr.update(value=status, elem_classes=classes), audio, video | |
| with gr.Blocks( | |
| title="Avatar Demo", | |
| theme=build_theme(), | |
| css=CUSTOM_CSS, | |
| ) as demo: | |
| gr.HTML( | |
| """ | |
| <div class="app-header"> | |
| <div class="app-header-accent"></div> | |
| <div> | |
| <h1>Avatar Demo</h1> | |
| <p>Type a line, upload a face, pick a voice — get a talking avatar video.</p> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| with gr.Row(elem_classes=["app-row"]): | |
| # ---------------- Left panel: inputs ---------------- | |
| with gr.Column(scale=5, elem_classes=["panel"]): | |
| gr.HTML('<div class="panel-title"><span class="step">1</span>Create your avatar</div>') | |
| gr.HTML('<div class="section-label">What should they say?</div>') | |
| text_in = gr.Textbox( | |
| label="Text", | |
| placeholder="Type what the avatar should say...", | |
| lines=4, | |
| max_lines=8, | |
| show_label=False, | |
| ) | |
| gr.HTML('<div class="section-label">Upload a face (portrait)</div>') | |
| with gr.Row(): | |
| image_in = gr.File( | |
| label="Reference image", | |
| type="filepath", | |
| file_count="single", | |
| elem_id="reference-image-upload", | |
| show_label=False, | |
| height=160, | |
| ) | |
| image_preview = gr.Image( | |
| label="Image preview", | |
| type="filepath", | |
| interactive=False, | |
| height=160, | |
| elem_id="reference-image-preview", | |
| show_download_button=False, | |
| show_share_button=False, | |
| ) | |
| gr.HTML('<div class="section-label">Choose a voice</div>') | |
| voice_mode = gr.Radio( | |
| choices=[MODE_PRESET, MODE_CLONE], | |
| value=MODE_PRESET, | |
| label="Voice source", | |
| show_label=False, | |
| ) | |
| speaker_in = gr.Dropdown( | |
| choices=speaker_choices, | |
| value=gepard_engine.speakers.names[0] if gepard_engine.speakers.names else None, | |
| label="Preset voice", | |
| visible=True, | |
| ) | |
| reference_audio_in = gr.Audio( | |
| sources=["upload", "microphone"], | |
| type="filepath", | |
| label=f"Reference voice (up to {int(gepard_config.max_ref_seconds)}s used)", | |
| visible=False, | |
| ) | |
| with gr.Accordion("Advanced settings", open=False): | |
| prompt_in = gr.Textbox( | |
| label="Video prompt", | |
| value=default_prompt, | |
| placeholder="Describe the desired motion in plain language.", | |
| lines=2, | |
| ) | |
| with gr.Row(): | |
| resolution_in = gr.Radio(["480p", "720p"], value="480p", label="Resolution") | |
| seed_in = gr.Number(value=42, precision=0, label="Seed") | |
| temperature_in = gr.Slider( | |
| 0.05, | |
| 1.0, | |
| value=gepard_config.defaults.temperature, | |
| step=0.05, | |
| label="Speech temperature", | |
| ) | |
| max_speech_frames_in = gr.Slider( | |
| 43, | |
| gepard_config.defaults.max_frames, | |
| value=215, | |
| step=43, | |
| label="Speech frame cap", | |
| ) | |
| repetition_penalty_in = gr.Slider( | |
| 1.0, | |
| 1.5, | |
| value=gepard_config.defaults.repetition_penalty, | |
| step=0.01, | |
| label="Repetition penalty", | |
| ) | |
| repetition_window_in = gr.Slider( | |
| 0, | |
| 128, | |
| value=gepard_config.defaults.repetition_window, | |
| step=4, | |
| label="Repetition window", | |
| ) | |
| acceleration_in = gr.Radio( | |
| [ACCEL_MODE_EXACT, ACCEL_MODE_DBCACHE, ACCEL_MODE_DBCACHE_FASTER], | |
| value=ACCEL_MODE_EXACT, | |
| label="Video acceleration", | |
| ) | |
| generate_btn = gr.Button( | |
| "Generate Talking Avatar", | |
| variant="primary", | |
| elem_id="generate-btn", | |
| size="lg", | |
| ) | |
| # ---------------- Right panel: outputs ---------------- | |
| with gr.Column(scale=5, elem_classes=["panel"]): | |
| gr.HTML('<div class="panel-title muted"><span class="step">2</span>Result</div>') | |
| gr.HTML( | |
| """ | |
| <div class="pipeline-steps"> | |
| <div class="pipeline-step"> | |
| <div class="ps-title"><span class="ps-num">1</span>Generate speech</div> | |
| <p class="ps-desc">GEPARD synthesizes a voice from your text.</p> | |
| </div> | |
| <div class="pipeline-step"> | |
| <div class="ps-title"><span class="ps-num">2</span>Generate video</div> | |
| <p class="ps-desc">LongCat animates the face to match the audio.</p> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| status_md = gr.Markdown( | |
| "Press **Generate** to start. The progress bar shows each step live as it runs.", | |
| elem_classes=["status-box"], | |
| ) | |
| video_out = gr.Video( | |
| label="Talking avatar video", | |
| autoplay=True, | |
| height=440, | |
| elem_classes=["result-video"], | |
| ) | |
| audio_out = gr.Audio( | |
| label="Step 1 preview · Generated speech", | |
| type="filepath", | |
| interactive=False, | |
| ) | |
| if example_choices: | |
| with gr.Column(elem_classes=["example-section"]): | |
| gr.Markdown("### Try a cached example") | |
| with gr.Row(elem_classes=["example-card"]): | |
| gr.Image( | |
| value=str(example_image), | |
| label="Example avatar", | |
| type="filepath", | |
| interactive=False, | |
| height=120, | |
| show_download_button=False, | |
| show_share_button=False, | |
| ) | |
| with gr.Column(scale=4): | |
| example_select = gr.Dropdown( | |
| choices=example_choices, | |
| value=example_choices[0][1], | |
| label="Preset", | |
| ) | |
| run_example_btn = gr.Button( | |
| "Load cached example", | |
| variant="secondary", | |
| ) | |
| example_outputs = [ | |
| text_in, | |
| image_in, | |
| image_preview, | |
| voice_mode, | |
| speaker_in, | |
| reference_audio_in, | |
| prompt_in, | |
| resolution_in, | |
| seed_in, | |
| temperature_in, | |
| max_speech_frames_in, | |
| repetition_penalty_in, | |
| repetition_window_in, | |
| acceleration_in, | |
| audio_out, | |
| video_out, | |
| status_md, | |
| ] | |
| run_example_btn.click( | |
| fn=run_cached_example, | |
| inputs=[example_select], | |
| outputs=example_outputs, | |
| api_name="run_cached_example", | |
| ) | |
| gr.HTML( | |
| """ | |
| <p class="footer-note">GEPARD TTS + LongCat-Video-Avatar · runs on ZeroGPU</p> | |
| <script> | |
| (() => { | |
| const setAccept = () => { | |
| document.querySelectorAll('#reference-image-upload input[type="file"]').forEach((input) => { | |
| input.accept = 'image/*'; | |
| }); | |
| }; | |
| setAccept(); | |
| new MutationObserver(setAccept).observe(document.body, { childList: true, subtree: true }); | |
| })(); | |
| </script> | |
| """ | |
| ) | |
| image_in.change( | |
| fn=preview_reference_image, | |
| inputs=[image_in], | |
| outputs=[image_preview], | |
| ) | |
| voice_mode.change( | |
| fn=_toggle_voice_mode, | |
| inputs=[voice_mode], | |
| outputs=[speaker_in, reference_audio_in], | |
| show_progress="hidden", | |
| ) | |
| generate_btn.click( | |
| fn=generate_talking_avatar, | |
| inputs=[ | |
| text_in, | |
| image_in, | |
| voice_mode, | |
| speaker_in, | |
| reference_audio_in, | |
| prompt_in, | |
| resolution_in, | |
| seed_in, | |
| temperature_in, | |
| max_speech_frames_in, | |
| repetition_penalty_in, | |
| repetition_window_in, | |
| acceleration_in, | |
| ], | |
| outputs=[audio_out, video_out], | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue(max_size=8).launch(show_error=True) | |