File size: 6,415 Bytes
77f7dfd c3b59db 77f7dfd 2a9234b 77f7dfd c3b59db 77f7dfd c3b59db 77f7dfd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | """Inference engine — a thin, device-agnostic layer over ``qwen_tts``.
The same functions run on MPS and CUDA; the only device knowledge lives in
``qvs.device``. Adds what the raw wrapper lacks for a studio: explicit seeding,
long-form sentence chunking, and a uniform return of ``(waveform, sample_rate)``.
"""
from __future__ import annotations
import re
from dataclasses import asdict, dataclass
from typing import Any, Optional
import numpy as np
from . import audio
from .config import GEN_DEFAULTS, LONGFORM_CHAR_THRESHOLD, OUTPUT_SAMPLE_RATE
from .device import get_attn_impl, get_dtype, target_device
# ---- generation parameters ----------------------------------------------------
@dataclass
class GenParams:
temperature: float = GEN_DEFAULTS.temperature
top_p: float = GEN_DEFAULTS.top_p
top_k: int = GEN_DEFAULTS.top_k
repetition_penalty: float = GEN_DEFAULTS.repetition_penalty
subtalker_temperature: float = GEN_DEFAULTS.subtalker_temperature
subtalker_top_p: float = GEN_DEFAULTS.subtalker_top_p
subtalker_top_k: int = GEN_DEFAULTS.subtalker_top_k
max_new_tokens: int = GEN_DEFAULTS.max_new_tokens
seed: int = GEN_DEFAULTS.seed
def to_kwargs(self) -> dict[str, Any]:
d = asdict(self)
d.pop("seed", None)
d["do_sample"] = self.temperature is not None and self.temperature > 0
return d
# ---- model loading / placement ------------------------------------------------
def load_model(repo: str, device: Optional[str] = None, load_on_cpu: bool = False):
"""Load a checkpoint. ``load_on_cpu=True`` builds on CPU (ZeroGPU: move to
cuda inside the ``@spaces.GPU`` fork afterwards)."""
from qwen_tts import Qwen3TTSModel
device = device or target_device()
device_map = "cpu" if load_on_cpu else device
return Qwen3TTSModel.from_pretrained(
repo,
device_map=device_map,
dtype=get_dtype(),
attn_implementation=get_attn_impl(device),
)
def move_model(model, device: str):
"""Relocate a loaded model (used to move CPU-built models onto the GPU)."""
import torch
model.model.to(device)
model.device = torch.device(device)
return model
# ---- seeding ------------------------------------------------------------------
def apply_seed(seed: int) -> None:
if seed is None or seed < 0:
return
import torch
torch.manual_seed(seed)
if torch.backends.mps.is_available():
try:
torch.mps.manual_seed(seed)
except Exception:
pass
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
def free_cache() -> None:
"""Release device cache between generations to keep committed memory bounded
(MPS accumulates intermediate buffers across sequential long-form chunks)."""
import torch
try:
if torch.backends.mps.is_available():
torch.mps.empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
except Exception:
pass
# ---- long-form chunking -------------------------------------------------------
# \s* (not \s+): CJK sentences have no whitespace after 。!?, so \s+ would never
# split them and long-form would collapse to one chunk.
_SENT_SPLIT = re.compile(r"(?<=[.!?。!?…])\s*")
def split_sentences(text: str) -> list[str]:
return [p for p in _SENT_SPLIT.split(text.strip()) if p.strip()]
def chunk_text(text: str, threshold: int = LONGFORM_CHAR_THRESHOLD) -> list[str]:
"""Group sentences into chunks no longer than ``threshold`` characters."""
if len(text) <= threshold:
return [text]
chunks: list[str] = []
cur = ""
for s in split_sentences(text):
if cur and len(cur) + len(s) + 1 > threshold:
chunks.append(cur)
cur = s
else:
cur = f"{cur} {s}".strip() if cur else s
if cur:
chunks.append(cur)
return chunks or [text]
def _run(model, method: str, texts: list[str], params: GenParams, **fixed) -> tuple[np.ndarray, int]:
"""Call a ``generate_*`` method once per chunk and concatenate."""
apply_seed(params.seed)
fn = getattr(model, method)
wavs_out: list[np.ndarray] = []
sr = OUTPUT_SAMPLE_RATE
for t in texts:
wavs, sr = fn(text=t, **fixed, **params.to_kwargs())
wavs_out.append(np.asarray(wavs[0], dtype=np.float32))
free_cache()
return audio.concat(wavs_out, sr), sr
# ---- the three modes ----------------------------------------------------------
def synth_custom_voice(model, text: str, speaker: str, instruct: Optional[str], language: str,
params: GenParams, longform: bool = True) -> tuple[np.ndarray, int]:
texts = chunk_text(text) if longform else [text]
return _run(model, "generate_custom_voice", texts, params,
speaker=speaker, instruct=(instruct or None), language=language)
def synth_voice_design(model, text: str, instruct: str, language: str,
params: GenParams, longform: bool = True) -> tuple[np.ndarray, int]:
texts = chunk_text(text) if longform else [text]
return _run(model, "generate_voice_design", texts, params,
instruct=instruct, language=language)
def synth_clone(model, text: str, language: str, params: GenParams,
ref_audio=None, ref_text: Optional[str] = None,
x_vector_only: bool = False, voice_clone_prompt=None,
longform: bool = True) -> tuple[np.ndarray, int]:
"""Clone. Provide either (ref_audio[, ref_text]) or a prebuilt
``voice_clone_prompt`` (from the voice library)."""
texts = chunk_text(text) if longform else [text]
apply_seed(params.seed)
wavs_out: list[np.ndarray] = []
sr = OUTPUT_SAMPLE_RATE
# Build the reusable prompt once so features aren't re-extracted per chunk.
if voice_clone_prompt is None and ref_audio is not None:
voice_clone_prompt = model.create_voice_clone_prompt(
ref_audio=ref_audio, ref_text=ref_text, x_vector_only_mode=x_vector_only
)
for t in texts:
wavs, sr = model.generate_voice_clone(
text=t, language=language, voice_clone_prompt=voice_clone_prompt, **params.to_kwargs()
)
wavs_out.append(np.asarray(wavs[0], dtype=np.float32))
free_cache()
return audio.concat(wavs_out, sr), sr
|