CoolFace
Apppublic

akhaliq/Dramabox

sourceHugging Faceotherupdated 4mo agoView on Hugging Face
5likes
inference_server.py381 linesDownload Raw Back to src
1#!/usr/bin/env python32"""3Warm TTS server — loads models once, accepts requests via stdin or function call.4 5The key insight: inference.py spends 11s on Gemma + 8s on model load every call.6This server loads everything once and keeps it warm.7 8We import and call the same code paths as inference.py but cache the heavy objects.9"""10import json11import logging12import os13import re14import sys15import time16from pathlib import Path17 18import torch19import torchaudio20 21# Setup paths22APP_DIR = Path(__file__).parent.parent23sys.path.insert(0, str(APP_DIR / "ltx2"))24sys.path.insert(0, str(APP_DIR / "src"))25 26logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")27 28from audio_conditioning import AudioConditionByReferenceLatent29from ltx_core.components.noisers import GaussianNoiser30from ltx_core.components.patchifiers import AudioPatchifier31from ltx_core.components.guiders import MultiModalGuider, MultiModalGuiderParams32from ltx_core.components.schedulers import LTX2Scheduler33from ltx_core.components.diffusion_steps import EulerDiffusionStep34from ltx_core.loader import DummyRegistry35from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder as Builder36from ltx_core.loader.sd_ops import SDOps37from ltx_core.model.transformer.model import LTXModel, LTXModelType, X0Model38from ltx_core.model.transformer.rope import LTXRopeType39from ltx_core.model.transformer.text_projection import create_caption_projection40from ltx_core.model.transformer.attention import AttentionFunction41from ltx_core.model.model_protocol import ModelConfigurator42from ltx_core.tools import AudioLatentTools43from ltx_core.types import Audio, AudioLatentShape, VideoPixelShape44from ltx_core.model.audio_vae import encode_audio as vae_encode_audio45from ltx_pipelines.utils.blocks import AudioConditioner, AudioDecoder, PromptEncoder46from ltx_pipelines.utils.media_io import decode_audio_from_file47from ltx_pipelines.utils.denoisers import GuidedDenoiser48from ltx_pipelines.utils.samplers import euler_denoising_loop49from safetensors import safe_open50 51 52DEFAULT_NEG = "worst quality, inconsistent, robotic, distorted, noise, static, muffled, unclear, unnatural, monotone"53 54 55def estimate_duration(prompt, multiplier=1.1):56    """Defer to the richer CLI estimator (sentence-aware + non-verbal action57    budget) so warm-server outputs match the lengths of the per-call CLI runs."""58    from inference import estimate_speech_duration59    base = estimate_speech_duration(prompt)60    return max(3.0, round(base * multiplier, 1))61 62 63def auto_rescale_for_cfg(cfg: float) -> float:64    """CFG-aware std-rescale schedule that prevents output clipping at high cfg.65 66    The CFG formula `pred = cond + (cfg-1)*(cond - uncond)` makes pred.std()67    grow roughly linearly with cfg, which the audio VAE+vocoder render as68    progressively louder waveforms. By cfg≈3 the output starts hard-clipping69    at 0 dBFS — and clipped information is unrecoverable in post.70 71    Empirical sweep on the blues prompt with the back-porch-boogie ref72    (rescale_scale needed for ≥1 dB peak headroom):73        cfg=2.5 → 0.2 ;  cfg=3 → 0.6 ; cfg=4 → 0.8 ; cfg=5–8 → 0.8 ; cfg=10 → 1.074 75    Piecewise-linear fit through those points; returns 0 below cfg=2 (no CFG76    even applied at cfg=1), plateaus at 0.8 between cfg=4 and cfg=8 to77    preserve the "extra punch" of high-CFG generations, and ramps to 1.0 by78    cfg=10.79    """80    if cfg <= 2.0:81        return 0.082    if cfg <= 3.0:83        return 0.6 * (cfg - 2.0)               # 0 → 0.684    if cfg <= 4.0:85        return 0.6 + 0.2 * (cfg - 3.0)         # 0.6 → 0.886    if cfg <= 8.0:87        return 0.8                              # plateau88    return min(1.0, 0.8 + 0.1 * (cfg - 8.0))   # 0.8 → 1.0 at cfg=1089 90 91class TTSServer:92    def __init__(self, checkpoint=None, full_checkpoint=None, gemma_root=None,93                 device="cuda", dtype="bf16", compile_model=True, bnb_4bit=True):94        MODELS = APP_DIR / "models"95        self.checkpoint = checkpoint or str(MODELS / "ltx-2.3-22b-dev-audio-only-v13-merged.safetensors")96        self.full_checkpoint = full_checkpoint or os.environ.get(97            "LTX_FULL_CHECKPOINT", "/mnt/persistent0/manmay/models/ltx23/ltx-2.3-22b-dev.safetensors")98        if gemma_root is None and not os.environ.get("GEMMA_DIR"):99            from model_downloader import get_gemma_path100            gemma_root = get_gemma_path()101        self.gemma_root = gemma_root or os.environ["GEMMA_DIR"]102        self.device = torch.device(device)103        self.dtype = torch.float16 if dtype == "fp16" else torch.bfloat16104        self.compile_model = compile_model105        self.bnb_4bit = bnb_4bit106        self.patchifier = AudioPatchifier(patch_size=1)107 108        # Cached models109        self._prompt_encoder = None110        self._velocity_model = None111        self._audio_conditioner = None112        self._audio_decoder = None113 114        logging.info(f"TTSServer loading on {device}...")115        t0 = time.time()116        self._load_all()117        logging.info(f"All models loaded in {time.time()-t0:.1f}s — ready for requests")118 119    def _load_all(self):120        # 1. Prompt encoder (Gemma + embeddings processor kept warm)121        t0 = time.time()122        self._prompt_encoder = PromptEncoder(123            checkpoint_path=self.full_checkpoint,124            gemma_root=self.gemma_root,125            dtype=self.dtype, device=self.device,126            warm=True,127            use_bnb_4bit=self.bnb_4bit,128            audio_only=True,129        )130        logging.info(f"  PromptEncoder (warm): {time.time()-t0:.1f}s")131 132        # 2. Audio conditioner (VAE encoder kept warm)133        t0 = time.time()134        self._audio_conditioner = AudioConditioner(135            checkpoint_path=self.full_checkpoint,136            dtype=self.dtype, device=self.device,137            warm=True,138        )139        logging.info(f"  AudioConditioner (warm): {time.time()-t0:.1f}s")140 141        # 3. Transformer142        t0 = time.time()143        with safe_open(self.checkpoint, framework="pt") as f:144            config = json.loads(f.metadata()["config"])145 146        t = config.get("transformer", {})147 148        class AudioOnlyConfigurator(ModelConfigurator[LTXModel]):149            @classmethod150            def from_config(cls, cfg):151                t = cfg.get("transformer", {})152                cp = None153                if not t.get("caption_proj_before_connector", False):154                    with torch.device("meta"):155                        cp = create_caption_projection(t, audio=True)156                return LTXModel(157                    model_type=LTXModelType.AudioOnly,158                    audio_num_attention_heads=t.get("audio_num_attention_heads", 32),159                    audio_attention_head_dim=t.get("audio_attention_head_dim", 64),160                    audio_in_channels=t.get("audio_in_channels", 128),161                    audio_out_channels=t.get("audio_out_channels", 128),162                    num_layers=t.get("num_layers", 48),163                    audio_cross_attention_dim=t.get("audio_cross_attention_dim", 2048),164                    norm_eps=t.get("norm_eps", 1e-6),165                    attention_type=AttentionFunction(t.get("attention_type", "default")),166                    positional_embedding_theta=10000.0,167                    audio_positional_embedding_max_pos=[20.0],168                    timestep_scale_multiplier=t.get("timestep_scale_multiplier", 1000),169                    use_middle_indices_grid=t.get("use_middle_indices_grid", True),170                    rope_type=LTXRopeType(t.get("rope_type", "interleaved")),171                    double_precision_rope=t.get("frequencies_precision", False) == "float64",172                    apply_gated_attention=t.get("apply_gated_attention", False),173                    audio_caption_projection=cp,174                    cross_attention_adaln=t.get("cross_attention_adaln", False),175                )176 177        audio_sd_ops = SDOps("AO").with_matching(prefix="model.diffusion_model.").with_replacement(178            "model.diffusion_model.", "")179        builder = Builder(180            model_path=self.checkpoint,181            model_class_configurator=AudioOnlyConfigurator,182            model_sd_ops=audio_sd_ops,183            registry=DummyRegistry(),184        )185        self._velocity_model = builder.build(device=self.device, dtype=self.dtype).to(self.device).eval()186        n_params = sum(p.numel() for p in self._velocity_model.parameters()) / 1e9187        vram_gb = sum(p.numel() * p.element_size() for p in self._velocity_model.parameters()) / 1e9188        logging.info(f"  Transformer: {time.time()-t0:.1f}s ({n_params:.1f}B params, {vram_gb:.1f}GB VRAM, {self.dtype})")189 190        # torch.compile for faster denoising191        if self.compile_model:192            t0 = time.time()193            logging.info("  Compiling transformer with torch.compile (default mode)...")194            self._velocity_model = torch.compile(self._velocity_model, mode="default", dynamic=True)195            logging.info(f"  Compiled: {time.time()-t0:.1f}s (first call triggers actual compilation)")196 197        # 4. Audio decoder (VAE decoder + vocoder kept warm)198        t0 = time.time()199        self._audio_decoder = AudioDecoder(200            checkpoint_path=self.full_checkpoint,201            dtype=self.dtype, device=self.device,202            warm=True,203        )204        logging.info(f"  AudioDecoder (warm): {time.time()-t0:.1f}s")205 206    @torch.inference_mode()207    def generate(self, prompt, voice_ref=None, cfg_scale=2.5, stg_scale=1.5,208                 duration_multiplier=1.1, seed=42, ref_duration=10.0,209                 rescale_scale="auto", gen_duration: float = 0.0):210        """Generate audio. Returns (waveform_path, duration_seconds).211 212        rescale_scale: latent-side CFG std-rescale that prevents clipping at213            high cfg. Set to "auto" (default) for the cfg-aware schedule, a214            float in [0, 1] for a fixed override, or 0 to disable.215        gen_duration: explicit target duration in seconds. 0 (default) → auto216            from prompt + duration_multiplier; >0 overrides everything else.217        """218        t_total = time.time()219 220        # Duration + target shape — explicit gen_duration wins over the estimator.221        if gen_duration and gen_duration > 0:222            gen_dur = float(gen_duration)223        else:224            gen_dur = estimate_duration(prompt, duration_multiplier)225        fps = 25.0226        n_frames = int(round(gen_dur * fps)) + 1227        n_frames = ((n_frames - 1 + 4) // 8) * 8 + 1228        pixel_shape = VideoPixelShape(batch=1, frames=n_frames, height=64, width=64, fps=fps)229        target_shape = AudioLatentShape.from_video_pixel_shape(pixel_shape)230        audio_tools = AudioLatentTools(patchifier=self.patchifier, target_shape=target_shape)231 232        # Initial state233        state = audio_tools.create_initial_state(device=self.device, dtype=self.dtype)234 235        # Voice ref conditioning236        if voice_ref and os.path.exists(voice_ref):237            t0 = time.time()238            voice = decode_audio_from_file(voice_ref, self.device, 0.0, ref_duration)239            w = voice.waveform240            if w.dim() == 2:241                if w.shape[0] == 1:242                    w = w.repeat(2, 1)243                w = w.unsqueeze(0)244            elif w.dim() == 3 and w.shape[1] == 1:245                w = w.repeat(1, 2, 1)246            target_samples = int(ref_duration * voice.sampling_rate)247            if w.shape[-1] < target_samples:248                w = w.repeat(1, 1, (target_samples // w.shape[-1]) + 1)249            w = w[..., :target_samples]250            peak = w.abs().max()251            if peak > 0:252                w = w * (10 ** (-4.0 / 20) / peak)253            voice = Audio(waveform=w, sampling_rate=voice.sampling_rate)254            ref_latent = self._audio_conditioner(lambda enc: vae_encode_audio(voice, enc, None))255            cond = AudioConditionByReferenceLatent(latent=ref_latent.to(self.device, self.dtype), strength=1.0)256            state = cond.apply_to(state, audio_tools)257            logging.info(f"Voice ref: {time.time()-t0:.2f}s")258 259        # Noise260        gen = torch.Generator(device=self.device).manual_seed(seed)261        noiser = GaussianNoiser(generator=gen)262        state = noiser(state, noise_scale=1.0)263 264        # Prompt encode265        t0 = time.time()266        prompts = [prompt, DEFAULT_NEG] if cfg_scale > 1.0 else [prompt]267        ctx = self._prompt_encoder(prompts, streaming_prefetch_count=None)268        a_ctx = ctx[0].audio_encoding269        a_ctx_neg = ctx[1].audio_encoding if cfg_scale > 1.0 else None270        logging.info(f"Prompt: {time.time()-t0:.2f}s")271 272        # Denoiser273        resc = auto_rescale_for_cfg(cfg_scale) if rescale_scale == "auto" else float(rescale_scale)274        if rescale_scale == "auto":275            logging.info(f"Auto rescale_scale = {resc:.2f} for cfg={cfg_scale}")276        guider = MultiModalGuider(277            params=MultiModalGuiderParams(278                cfg_scale=cfg_scale, stg_scale=stg_scale,279                stg_blocks=[29], rescale_scale=resc, modality_scale=1.0,280            ),281            negative_context=a_ctx_neg,282        )283        denoiser = GuidedDenoiser(284            v_context=None, a_context=a_ctx,285            video_guider=None, audio_guider=guider,286        )287 288        # Sigmas289        sigmas = LTX2Scheduler().execute(steps=30, latent=state.latent).to(self.device)290 291        # Denoise292        t0 = time.time()293        x0 = X0Model(self._velocity_model)294        _, audio_state = euler_denoising_loop(295            sigmas=sigmas, video_state=None, audio_state=state,296            stepper=EulerDiffusionStep(), transformer=x0, denoiser=denoiser,297        )298        logging.info(f"Denoise (30 steps): {time.time()-t0:.2f}s")299 300        # Strip + unpatchify + decode301        audio_state = audio_tools.clear_conditioning(audio_state)302        audio_state = audio_tools.unpatchify(audio_state)303 304        # End-of-clip silence-prior fix.305        # The base LTX-2.3 22B DiT was trained on audio clips ≤ ~20 s and306        # learned a strong "clip-end silence" prior that lands on the next307        # patchifier-aligned latent frame after 20 s — index 513 = 8*64+1.308        # When inference produces longer audio, this prior leaks through as a309        # high-norm latent burst at frame 513 (and adjacent 512), which the310        # audio VAE + vocoder render as a ~30 ms hard silence dip near 20.4 s.311        # Linear interpolation across the two affected frames removes the dip312        # cleanly without any retraining. Only runs when the latent is long313        # enough to actually contain the boundary.314        latent = audio_state.latent315        if latent.shape[2] > 513:316            f0, f1 = 511, 514          # neighbours used for interpolation317            n = f1 - f0                # = 3318            patched = latent.clone()319            for f in (512, 513):320                t = (f - f0) / n321                patched[:, :, f, :] = (1.0 - t) * latent[:, :, f0, :] + t * latent[:, :, f1, :]322            latent = patched323 324        t0 = time.time()325        decoded = self._audio_decoder(latent)326        logging.info(f"Decode: {time.time()-t0:.2f}s")327 328        total = time.time() - t_total329        dur = decoded.waveform.shape[-1] / decoded.sampling_rate330        logging.info(f"Total: {total:.2f}s for {dur:.1f}s audio")331        return decoded.waveform, decoded.sampling_rate332 333    def generate_to_file(self, prompt, output, watermark: bool = True, **kwargs):334        waveform, sr = self.generate(prompt, **kwargs)335        wav_cpu = waveform.cpu().float()336        if watermark:337            try:338                import numpy as np, perth339                if not hasattr(self, "_perth"):340                    self._perth = perth.PerthImplicitWatermarker()341                mono = wav_cpu.mean(dim=0).numpy() if wav_cpu.shape[0] > 1 else wav_cpu[0].numpy()342                mono_wm = self._perth.apply_watermark(mono, sample_rate=sr)343                mono_wm_t = torch.from_numpy(np.asarray(mono_wm, dtype=np.float32)).unsqueeze(0)344                wav_cpu = mono_wm_t if wav_cpu.shape[0] == 1 else mono_wm_t.repeat(wav_cpu.shape[0], 1)345            except Exception as e:346                logging.warning(f"Perth watermark skipped ({e})")347        torchaudio.save(output, wav_cpu, sr)348        logging.info(f"Saved: {output}")349        return output350 351 352if __name__ == "__main__":353    import argparse354    p = argparse.ArgumentParser()355    p.add_argument("--device", default="cuda")356    p.add_argument("--dtype", default="fp16", choices=["fp16", "bf16"])357    p.add_argument("--no-compile", action="store_true")358    p.add_argument("--no-bnb-4bit", action="store_true",359                   help="Disable bitsandbytes 4-bit path (default: on, since the default "360                        "unsloth Gemma checkpoint is pre-quantized).")361    args = p.parse_args()362 363    server = TTSServer(device=args.device, dtype=args.dtype, compile_model=not args.no_compile,364                       bnb_4bit=not args.no_bnb_4bit)365 366    # First call - includes any warmup367    logging.info("=== First request ===")368    server.generate_to_file(369        prompt='A woman speaks clearly, "The weather today will be sunny."',370        output="/tmp/warm_test1.wav",371        voice_ref="/mnt/persistent0/manmay/expressive/female_radio_nikole/female_radio_nikole.wav",372    )373 374    # Second call - should be much faster (models already warm)375    logging.info("\n=== Second request (warm) ===")376    server.generate_to_file(377        prompt='A man speaks excitedly, "This is amazing, I cannot believe it!"',378        output="/tmp/warm_test2.wav",379        voice_ref="/mnt/persistent0/manmay/expressive/male_arnie/male_arnie.mp3",380    )381