Instructions to use justanurag/maya-mac with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use justanurag/maya-mac with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] hf download justanurag/maya-mac --local-dir maya-mac
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
- Atomic Chat
Download src/maya_mac/engine.py from justanurag/maya-mac: direct link, hf CLI and curl.
- Browser
- Download file 26.7 kB
-
https://huggingface.co/justanurag/maya-mac/resolve/main/src/maya_mac/engine.py
- Command line
-
hf download hf://justanurag/maya-mac/src/maya_mac/engine.py
-
curl -L -o engine.py https://huggingface.co/justanurag/maya-mac/resolve/main/src/maya_mac/engine.py
26.7 kB
| """Single-request Maya inference engine for Apple Silicon (MPS) and CPU. | |
| Same model, prompt geometry, cb0 sampler, controlled stop and guards as the upstream CUDA engine | |
| (maya3/engine.py), minus everything that only exists on NVIDIA: no Triton kernels, no CUDA graphs, no seat batching. | |
| Per frame (80 ms of audio): | |
| 1. backbone : one Qwen3 decode step over the HF KV cache -> hidden h | |
| 2. cb0 : cb0 logits -> presence penalty / temperature / top-k / top-p -> controlled stop + sample (host side) | |
| 3. depth : levels 1..31 from h and cb0 (FastDepth: per-group KV cache, constant speaker cross-attention) | |
| 4. feedback : the frame's 32 codes are embedded and fed back to the backbone | |
| Mimi decodes the codes to 24 kHz audio (whole piece, or streamed by StreamingMimi). | |
| """ | |
| from __future__ import annotations | |
| import collections | |
| import dataclasses | |
| import os | |
| import time | |
| from concurrent.futures import ThreadPoolExecutor | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| from maya_mac import guards as G # noqa: E402 | |
| from maya_mac import reference as S # noqa: E402 | |
| from maya_mac.prompt import DEFAULT_BUILDER, MODES, Voice, VoiceRegistry, load_named_voices, norm_mode # noqa: E402 | |
| from maya_mac.tts.tokens import AEOS_CLASS, N_Q # noqa: E402 | |
| SR = 24000 | |
| FRAME = 1920 | |
| HERE = Path(__file__).resolve().parent | |
| # where each component comes from: a Hugging Face repo id, the files to fetch, and the files that must exist after | |
| REPOS = { | |
| "maya2-native": ("maya-research/maya2-native", ["*.safetensors", "*.safetensors.index.json", "maya3_config.json", | |
| "backbone/*", "voices/*", "LICENSE"], | |
| ["model.safetensors.index.json", "maya3_config.json", "backbone/config.json", | |
| "backbone/tokenizer.json"]), | |
| "mimi": ("kyutai/mimi", ["config.json", "model.safetensors", "preprocessor_config.json"], | |
| ["config.json", "model.safetensors"]), | |
| "ecapa": ("speechbrain/spkrec-ecapa-voxceleb", ["embedding_model.ckpt", "hyperparams.yaml"], | |
| ["embedding_model.ckpt"]), | |
| } | |
| def _missing(d, required): | |
| d = Path(d) | |
| out = [f for f in required if not (d / f).is_file()] | |
| index = d / "model.safetensors.index.json" | |
| if index.is_file(): # every shard the index names | |
| import json | |
| out += sorted({f for f in json.loads(index.read_text())["weight_map"].values() if not (d / f).is_file()}) | |
| return out | |
| def resolve_models(models_dir=None): | |
| """-> {component: local dir}. models_dir (or $MAYA_MODEL_DIR) holding maya2-native/, mimi/ and ecapa/ is used as | |
| is; otherwise each component is fetched into the Hugging Face cache (files already cached are not re-downloaded; | |
| with no network, or HF_HUB_OFFLINE=1, the cache alone is used).""" | |
| models_dir = models_dir or os.environ.get("MAYA_MODEL_DIR") | |
| if models_dir: | |
| d = Path(models_dir).expanduser() | |
| out = {n: d / n for n in REPOS} | |
| else: | |
| from huggingface_hub import snapshot_download | |
| out = {} | |
| for name, (repo, patterns, _) in REPOS.items(): | |
| try: | |
| out[name] = Path(snapshot_download(repo, allow_patterns=patterns)) | |
| except Exception: # offline: whatever the cache holds (checked below) | |
| out[name] = Path(snapshot_download(repo, allow_patterns=patterns, local_files_only=True)) | |
| for name, (repo, _, required) in REPOS.items(): | |
| gone = _missing(out[name], required) | |
| if gone: | |
| raise FileNotFoundError(f"{name} ({repo}) at {out[name]} lacks {gone}") | |
| return out | |
| def mlx_available(): | |
| import platform | |
| import sys | |
| if sys.platform != "darwin" or platform.machine() != "arm64": | |
| return False | |
| try: | |
| import mlx.core # noqa: F401 | |
| except ImportError: | |
| return False | |
| return True | |
| def default_device(): | |
| if torch.backends.mps.is_available(): | |
| return "mps" | |
| if torch.cuda.is_available(): | |
| return "cuda" | |
| return "cpu" | |
| class EngineConfig: | |
| depth_temp: float = 0.7 # depth sampler (upstream EngineConfig.temp / top_k) | |
| depth_top_k: int = 64 | |
| depth_dtype: str = "fp16" # fp16 | bf16 | fp32 (upstream default fp16) | |
| backbone_dtype: str = "fp16" # bf16 | fp16 | fp32 (CPU always runs fp32) | |
| # MPS SDPA returns wrong attention for this model in fp16/bf16 (~95 % relative error on the hidden states, | |
| # measured with torch 2.8.0); eager attention is accurate (fp16: 0.4 %, bf16: 2.5 % vs fp32) | |
| backbone_attn: str = "auto" # auto (eager on MPS, sdpa elsewhere) | eager | sdpa | |
| cb0_temp: float = 0.9 # cb0 sampler defaults = upstream sweep winner | |
| cb0_top_k: int = 64 | |
| cb0_top_p: float = 1.0 | |
| cb0_rep: float = 0.8 # presence penalty over the last cb0_rep_win frames | |
| cb0_rep_win: int = 12 | |
| hard_cap: int = 1500 # frames (120 s) per piece | |
| capacity: int = 4096 # backbone KV positions (prompt + generated frames of one piece) | |
| gap_s: float = 0.15 # silence between the text pieces of one request | |
| chunk_s: float = 20.0 # long text is split into pieces of about this many seconds | |
| stream_frames: int = 3 # frames per streamed audio chunk (240 ms) | |
| codec_device: str = "cpu" # Mimi decode/encode device | |
| backend: str = "auto" # auto (mlx when installed on Apple Silicon, else torch) | mlx | torch | |
| backbone_bits: int | None = None # mlx only: 8 | 4 = quantized backbone weights (None: backbone_dtype) | |
| depth_bits: int | None = None # mlx only: 8 | 4 = quantized depth decoder weights (None: depth_dtype) | |
| warmup: bool = True # run a short generation at load | |
| seed: int = 20261024 | |
| guards: G.GuardConfig = dataclasses.field(default_factory=G.GuardConfig) | |
| def load_audio(x, sr=None): | |
| """path | (array, sr) | array (assumed 24 kHz) -> float32 mono 24 kHz.""" | |
| if isinstance(x, (str, os.PathLike)): | |
| import soundfile as sf | |
| w, sr = sf.read(str(x), dtype="float32") | |
| elif isinstance(x, tuple): | |
| w, sr = x | |
| else: | |
| w = x | |
| w = np.asarray(w, np.float32) | |
| if w.ndim > 1: | |
| w = w.mean(1) if w.shape[1] < w.shape[0] else w.mean(0) | |
| sr = sr or SR | |
| if sr != SR: | |
| from math import gcd | |
| from scipy.signal import resample_poly | |
| g = gcd(int(sr), SR) | |
| w = resample_poly(w, SR // g, int(sr) // g).astype(np.float32) | |
| return w | |
| class Ecapa: | |
| """ECAPA-TDNN speaker embedding (speechbrain spkrec-ecapa-voxceleb; 16 kHz fbank-80, sentence norm).""" | |
| def __init__(self, ecapa_dir, device): | |
| from speechbrain.lobes.features import Fbank | |
| from speechbrain.lobes.models.ECAPA_TDNN import ECAPA_TDNN | |
| from speechbrain.processing.features import InputNormalization | |
| self.fbank = Fbank(n_mels=80).eval().to(device) | |
| self.norm = InputNormalization(norm_type="sentence", std_norm=False).eval().to(device) | |
| emb = ECAPA_TDNN(input_size=80, channels=[1024, 1024, 1024, 1024, 3072], kernel_sizes=[5, 3, 3, 3, 1], | |
| dilations=[1, 2, 3, 4, 1], attention_channels=128, lin_neurons=192).eval() | |
| emb.load_state_dict(torch.load(Path(ecapa_dir) / "embedding_model.ckpt", map_location="cpu", weights_only=True), | |
| strict=True) | |
| self.emb = emb.to(device) | |
| self.device = device | |
| def __call__(self, w24): | |
| from scipy.signal import resample_poly | |
| if w24 is None or len(w24) < 2400: | |
| raise ValueError("reference audio is too short (need >= 0.1 s; 3-10 s recommended)") | |
| w16 = torch.from_numpy(resample_poly(np.asarray(w24, np.float32), 2, 3).astype(np.float32)).to(self.device)[None] | |
| L = torch.ones(1, device=self.device) | |
| e = self.emb(self.norm(self.fbank(w16), L), L).reshape(-1) | |
| return torch.nn.functional.normalize(e, dim=0).cpu().numpy() | |
| class Cb0Sampler: | |
| """Upstream Engine._cb0 for one request, on the host: presence penalty, temperature, top-k, top-p, then the | |
| controlled stop (argmax AEOS / p_aeos >= 0.35 / hazard >= 0.6, after `minimum` frames) and a draw with AEOS removed.""" | |
| def __init__(self, cfg: EngineConfig, minimum, sampling, gen): | |
| self.temp = float(sampling.get("temperature") or cfg.cb0_temp) | |
| self.top_k = int(sampling.get("top_k") or cfg.cb0_top_k) | |
| self.top_p = float(sampling.get("top_p") or cfg.cb0_top_p) | |
| rp = sampling.get("repetition_penalty") | |
| self.rep = float(rp if rp is not None else cfg.cb0_rep) | |
| self.hist = np.full(max(1, cfg.cb0_rep_win), -1, np.int64) | |
| self.win = cfg.cb0_rep_win | |
| self.minimum = minimum | |
| self.hazard = 0.0 | |
| self.frames = 0 | |
| self.aeos_bias = 0.0 | |
| self.gen = gen | |
| def __call__(self, logits): | |
| """logits: (2049,) float32 CPU tensor -> (stop reason or None, cb0).""" | |
| logits = logits / self.temp | |
| logits[AEOS_CLASS] += self.aeos_bias | |
| if self.win > 0 and self.rep > 0: | |
| seen = self.hist[self.hist >= 0] | |
| if seen.size: | |
| seen = torch.from_numpy(np.unique(seen)) | |
| seen = seen[seen != AEOS_CLASS] | |
| logits[seen] -= self.rep | |
| k = min(self.top_k, logits.shape[0]) | |
| kth = logits.topk(k).values[-1] | |
| logits = logits.masked_fill(logits < kth, -float("inf")) | |
| if self.top_p < 1.0: | |
| sp, si = logits.softmax(-1).sort(descending=True) | |
| drop = (sp.cumsum(-1) - sp) > self.top_p | |
| logits[si[drop]] = -float("inf") | |
| probs = logits.softmax(-1) | |
| pa = float(probs[AEOS_CLASS]) | |
| stop = None | |
| if self.frames >= self.minimum: | |
| self.hazard = 1 - (1 - self.hazard) * (1 - pa) | |
| if int(probs.argmax()) == AEOS_CLASS: | |
| stop = "aeos_argmax" | |
| elif pa >= 0.35: | |
| stop = "aeos_tau" | |
| elif self.hazard >= 0.6: | |
| stop = "aeos_hazard" | |
| q = probs.clone() | |
| q[AEOS_CLASS] = 0 | |
| q = q / q.sum().clamp_min(1e-12) | |
| cb0 = int(torch.multinomial(q, 1, generator=self.gen)) | |
| if self.win > 0: | |
| self.hist[:-1] = self.hist[1:] | |
| self.hist[-1] = cb0 | |
| self.frames += 1 | |
| return stop, cb0 | |
| class MayaTTS: | |
| """from maya_mac import MayaTTS | |
| tts = MayaTTS.from_pretrained() # downloads on first use, MPS when available | |
| wav = tts.generate("नमस्ते, आप कैसे हैं?", lang="hi", voice="Ahana", normalize=True) # float32, 24 kHz | |
| for chunk in tts.stream("Hello there.", lang="en", voice="Ahana"): ... # float32 chunks | |
| tts.register_voice("me", ref_audio="me.wav", ref_text="...", lang="en") # zero-shot clone | |
| """ | |
| def __init__(self, model_dir, mimi_dir, ecapa_dir=None, device=None, config: EngineConfig = None, log=print): | |
| from transformers import MimiModel | |
| from maya_mac.tts.data.packing_v2 import PackerV2 | |
| from maya_mac.tts.model.load_s3 import load_model_for_eval | |
| from maya_mac.codec import StreamingMimi | |
| from maya_mac.fast_backbone import LeanBackbone | |
| from maya_mac.fast_depth import FastDepth | |
| self.device = device = device or default_device() | |
| self.cfg = cfg = config or EngineConfig() | |
| self.log = log | |
| t0 = time.perf_counter() | |
| attn = cfg.backbone_attn if cfg.backbone_attn != "auto" else ("eager" if device == "mps" else "sdpa") | |
| mcfg = {"model": {"backbone": str(Path(model_dir) / "backbone"), "attn": attn}, "data": {"max_tokens": 8192}} | |
| dtypes = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32} | |
| bb_dtype = torch.float32 if device == "cpu" else dtypes[cfg.backbone_dtype] | |
| self.dtype = bb_dtype | |
| self.model, _ = load_model_for_eval(str(model_dir), device=device, dtype=bb_dtype, cfg=mcfg, | |
| config=str(HERE / "configs/s3_fresh_v1.yaml"), amortize_frac=1.0, | |
| require_anchor=True, verbose=False) | |
| self.model.depth.float() | |
| depth_dtype = dtypes[cfg.depth_dtype] | |
| if device == "cpu": | |
| depth_dtype = torch.float32 | |
| self.depth = FastDepth(self.model.depth, dtype=depth_dtype, temp=cfg.depth_temp, top_k=cfg.depth_top_k) | |
| d = self.model.depth # only spk_proj_* / null_mem are used from here on; FastDepth holds its own copy | |
| for name in ("groups", "cross", "heads", "code_lo", "code_up", "in_proj", "level_emb", "norm"): | |
| setattr(d, name, None) | |
| # the lean backbone keeps its own fused q|k|v and gate|up weights (and references the rest); the HF decoder | |
| # layers are never called after this, so drop them instead of holding two copies | |
| self.backbone = LeanBackbone(self.model.backbone, capacity=cfg.capacity) | |
| self.model.backbone.lm.model.layers = torch.nn.ModuleList() | |
| if device == "mps": | |
| torch.mps.empty_cache() | |
| self.packer = PackerV2(self.model.backbone.tokenizer, self.model.backbone.token_ids, max_tokens=8192) | |
| self.prior = S.load_prior(str(HERE / "configs/text_frames_prior.json")) | |
| self.dur = G.DurationPrior() | |
| # Mimi runs on the CPU in a worker thread: off the GPU's critical path (MPS pays per kernel), and exact | |
| self.mimi = MimiModel.from_pretrained(str(mimi_dir)).to(cfg.codec_device).eval() | |
| self.codec = StreamingMimi(self.mimi) | |
| self._codec_pool = ThreadPoolExecutor(1, thread_name_prefix="maya-mimi") | |
| self.ecapa_dir = ecapa_dir | |
| self._ecapa = None | |
| self.registry = VoiceRegistry() | |
| voices_json = Path(model_dir) / "voices" / "voices.json" | |
| if voices_json.is_file(): | |
| load_named_voices(self.registry, str(voices_json)) | |
| self._fb_offsets = torch.arange(N_Q, device=device) * self.model.audio_in.n_codes | |
| self._fb_scale = float(N_Q) ** 0.5 | |
| if cfg.warmup: # MPS compiles its kernels on first use: pay that at load, not on the first request | |
| list(self.stream("Hello, this is a warmup.", "en", max_frames=6, seed=1)) | |
| self.load_s = time.perf_counter() - t0 | |
| self._seed_rng = np.random.default_rng(cfg.seed) | |
| self.last_info = None | |
| def from_pretrained(cls, models_dir=None, device=None, **config): | |
| """Weights come from the Hugging Face cache (downloaded on first use, ~5.5 GB), or from models_dir / | |
| $MAYA_MODEL_DIR holding maya2-native/, mimi/ and ecapa/. config: EngineConfig fields. | |
| device "mlx" (or backend="mlx") selects the MLX engine; with backend "auto" and no device it is used whenever | |
| MLX is installed on Apple Silicon, and an explicit torch device (mps | cpu | cuda) selects the torch engine.""" | |
| d = resolve_models(models_dir) | |
| cfg = EngineConfig(**config) | |
| if cls is MayaTTS: | |
| if device == "mlx" or cfg.backend == "mlx" or (cfg.backend == "auto" and device is None and mlx_available()): | |
| from maya_mac.mlx_engine import MayaMLX | |
| cls = MayaMLX | |
| return cls(d["maya2-native"], d["mimi"], d["ecapa"], device=device, config=cfg) | |
| # ------------------------------------------------------------------ voices | |
| def voices(self): | |
| return self.registry.ids() | |
| def prepare_voice(self, voice: Voice): | |
| """Pack the voice's reference prefix and compute its depth speaker memory (once per voice).""" | |
| with voice.lock: | |
| if voice.mem is not None: | |
| return voice | |
| _, use_mem = MODES[voice.mode] | |
| builder = voice.builder or DEFAULT_BUILDER | |
| voice.prefix = builder.prefix(self.packer, voice) | |
| voice.spk_refs = (np.asarray(voice.ecapa, np.float32).reshape(1, 192) if voice.ecapa is not None | |
| else np.zeros((1, 192), np.float32)) | |
| refs_t = torch.as_tensor(voice.spk_refs, device=self.device)[None] | |
| voice.mem = self.model.depth.spk_memory(refs_t, "ecapa", | |
| null_mask=torch.tensor([not use_mem], device=self.device)) | |
| voice.rmask = torch.ones(1, 1, dtype=torch.bool, device=self.device) | |
| voice.ts = int(voice.prefix["tokens"].shape[0]) if voice.prefix is not None else 0 | |
| return voice | |
| def _embed(self, w): | |
| if self._ecapa is None: | |
| if self.ecapa_dir is None: | |
| raise ValueError("ecapa_dir is required for reference-audio voices") | |
| self._ecapa = Ecapa(self.ecapa_dir, self.device) | |
| return self._ecapa(w) | |
| def make_voice(self, voice_id, ref_audio=None, ref_text=None, lang=None, mode="icl_spk", meta=None): | |
| """Build a Voice from a reference clip (audio + its transcript + its language); mode plain needs no clip.""" | |
| mode = norm_mode(mode) | |
| if mode == "plain": | |
| return self.prepare_voice(Voice(voice_id, mode, meta=meta or {})) | |
| if ref_audio is None: | |
| raise ValueError(f"mode {mode} needs reference audio") | |
| w = load_audio(ref_audio) | |
| if len(w) > 30 * SR: | |
| raise ValueError("reference audio longer than 30 s; 3-10 s works best") | |
| x = torch.as_tensor(w, device=self.cfg.codec_device)[None, None] | |
| codes = self.mimi.encode(x, num_quantizers=N_Q).audio_codes[0].cpu().numpy().astype(np.int64) | |
| ecapa = self._embed(w) if mode in ("spk", "icl_spk") else None | |
| clips = [] | |
| if mode in ("icl", "icl_spk"): | |
| if not ref_text or not lang: | |
| raise ValueError("ref_text and lang (the reference clip's transcript and language) are required") | |
| clips = [{"text": ref_text, "codes": codes, "lang": lang}] | |
| return self.prepare_voice(Voice(voice_id, mode, clips=clips, ecapa=ecapa, meta=meta or {})) | |
| def register_voice(self, voice_id, ref_audio=None, ref_text=None, lang=None, mode="icl_spk", meta=None): | |
| self.registry.add(self.make_voice(voice_id, ref_audio, ref_text, lang, mode, meta)) | |
| return voice_id | |
| def remove_voice(self, voice_id): | |
| return self.registry.remove(voice_id) is not None | |
| def close(self): | |
| self._codec_pool.shutdown(wait=True) | |
| # ------------------------------------------------------------------ synthesis | |
| def _plan(self, text, lang, voice, max_frames): | |
| cfg, g = self.cfg, self.cfg.guards | |
| builder = voice.builder or DEFAULT_BUILDER | |
| ex = builder.example(self.packer, voice, text, lang) | |
| prompt, _ = S.prompt_from_example(ex, self.model, self.device, self.dtype, N_Q) | |
| ef, er = S.expected_frames(self.prior, lang, len(text)) | |
| loose = min(cfg.hard_cap, int(2.0 * (ef + er) + 40)) | |
| cap = loose | |
| if g.enabled: | |
| cap = min(cap, max(int(ef * 0.5) + 8, self.dur.cap_frames(text, lang, g.dur_k, g.dur_margin_s))) | |
| if max_frames: | |
| cap = min(cap, max(1, int(max_frames))) | |
| minimum = max(4, int(0.5 * ef)) | |
| suspend = G.suspend_windows_v2(text, lang, self.dur) if (g.enabled and g.loop and g.aware_v2) else () | |
| return prompt.to(self.dtype), cap, loose, minimum, suspend | |
| def _frames(self, text, lang, voice, sampling, max_frames, info): | |
| """Yield the (32,) code tensor of each frame of one text piece; info receives the stop record.""" | |
| cfg, g = self.cfg, self.cfg.guards | |
| prompt, cap, loose, minimum, suspend = self._plan(text, lang, voice, max_frames) | |
| seed = sampling.get("seed") | |
| seed = int(seed) if seed is not None else int(self._seed_rng.integers(1, 2 ** 31 - 1)) | |
| cpu_gen = torch.Generator().manual_seed(seed) | |
| self.depth.set_speaker(voice.mem, seed=(seed * 69069 + 1) % (2 ** 31 - 1)) | |
| sampler = Cb0Sampler(cfg, minimum, sampling, cpu_gen) | |
| if prompt.shape[1] + cap + 1 > cfg.capacity: | |
| raise ValueError(f"prompt of {prompt.shape[1]} tokens + {cap} frames exceeds KV capacity {cfg.capacity}") | |
| backbone = self.backbone | |
| backbone.reset() | |
| h = backbone.forward(prompt) | |
| cb0_hist = np.full(g.hist, -1, np.int64) | |
| frames, reason, boost_at, n_boost = 0, "cap", None, 0 | |
| while True: | |
| logits = self.model.backbone.cb0_logits(h)[0].float().cpu() # the one host sync per frame | |
| stop, cb0 = sampler(logits) | |
| if stop: | |
| reason = stop | |
| break | |
| codes = self.depth(h, cb0) # (32,) long, on device | |
| yield codes | |
| frames += 1 | |
| cb0_hist[:-1] = cb0_hist[1:] | |
| cb0_hist[-1] = cb0 | |
| if frames >= cap: | |
| reason = "max_frames" if max_frames and frames >= max_frames else ( | |
| "cap_guard" if g.enabled and cap < loose else "cap") | |
| break | |
| if (g.enabled and g.loop and frames >= g.start and frames % g.every == 0 | |
| and not any(a <= frames <= b for a, b in suspend)): | |
| hit = G.loop_detect_batch(cb0_hist[None], np.array([frames]), g)[0] | |
| if hit is not None and g.mode == "boost": | |
| if boost_at is None: | |
| boost_at, n_boost = frames, n_boost + 1 | |
| sampler.aeos_bias = g.boost | |
| if frames - boost_at >= g.backstop: | |
| reason = "loop_backstop" | |
| break | |
| elif hit is not None: | |
| reason = "loop_" + hit.split(":")[0] | |
| break | |
| elif boost_at is not None: | |
| sampler.aeos_bias, boost_at = 0.0, None | |
| h = backbone.forward(self._feedback(codes)[None]) | |
| info.update({"reason": reason, "frames": frames, "cap": cap, "n_boost": n_boost}) | |
| def _feedback(self, codes): | |
| """== model.audio_in.frames(codes[None], 32) without its host-synchronizing range check (depth codes are | |
| always in range).""" | |
| a = self.model.audio_in | |
| return a.norm(a.table(codes + self._fb_offsets)[None].sum(1) / self._fb_scale) | |
| def _voice(self, voice, ref_audio, ref_text, ref_lang, mode): | |
| if ref_audio is not None: | |
| if voice not in (None, "plain"): | |
| raise ValueError("pass either voice=<named/registered voice> or ref_audio=, not both") | |
| return self.make_voice("oneoff", ref_audio, ref_text, ref_lang, mode) | |
| v = self.registry.get(voice or "plain") | |
| return self.prepare_voice(v) if v.mem is None else v | |
| def _prep(self, text, lang, normalize): | |
| if not isinstance(text, str) or not text.strip(): | |
| raise ValueError("text must be a non-empty string") | |
| text = text.strip() | |
| if normalize: | |
| from maya_mac.text import normalise_text | |
| text = normalise_text(text, lang) | |
| return S.split_text(text, lang, self.prior, self.cfg.chunk_s) if self.cfg.chunk_s > 0 else [text] | |
| def stream(self, text, lang="en", voice=None, ref_audio=None, ref_text=None, ref_lang=None, mode="icl_spk", | |
| normalize=False, max_frames=None, seed=None, temperature=None, top_k=None, top_p=None, | |
| repetition_penalty=None, info=None): | |
| """Yield float32 PCM chunks (24 kHz mono) as frames are generated.""" | |
| v = self._voice(voice, ref_audio, ref_text, ref_lang or lang, mode) | |
| sampling = {"seed": seed, "temperature": temperature, "top_k": top_k, "top_p": top_p, | |
| "repetition_penalty": repetition_penalty} | |
| pieces = self._prep(text, lang, normalize) | |
| t0, t_first, total, stops = time.perf_counter(), None, 0, [] | |
| for k, piece in enumerate(pieces): | |
| if k: | |
| yield np.zeros(int(self.cfg.gap_s * SR), np.float32) | |
| sp = dict(sampling) | |
| if seed is not None: # each piece gets its own derived stream (upstream rule) | |
| sp["seed"] = (int(seed) * 1000003 + k * 7919) % (2 ** 31 - 1) | |
| rec = {} | |
| left = (max_frames - total) if max_frames else None | |
| self._codec_pool.submit(self.codec.reset).result() | |
| buf, pending, n = [], collections.deque(), self.cfg.stream_frames | |
| for codes in self._frames(piece, lang, v, sp, left, rec): | |
| buf.append(codes) | |
| # a frame's codes are ready once the NEXT frame has been produced (its cb0 read syncs the device), | |
| # so chunks are copied to the host one frame late and never stall the GPU pipeline | |
| if len(buf) > n: | |
| chunk, buf = torch.stack(buf[:n], -1).cpu(), buf[n:] | |
| pending.append(self._codec_pool.submit(self.codec.decode, chunk)) | |
| while pending and pending[0].done(): | |
| if t_first is None: | |
| t_first = time.perf_counter() | |
| yield pending.popleft().result() | |
| if buf: | |
| pending.append(self._codec_pool.submit(self.codec.decode, torch.stack(buf, -1).cpu())) | |
| while pending: | |
| pcm = pending.popleft().result() | |
| if t_first is None: | |
| t_first = time.perf_counter() | |
| yield pcm | |
| total += rec["frames"] | |
| stops.append(rec) | |
| if max_frames and total >= max_frames: | |
| break | |
| wall = time.perf_counter() - t0 | |
| audio_s = total * FRAME / SR | |
| self.last_info = {"reason": stops[-1]["reason"] if stops else None, "frames": total, "audio_s": audio_s, | |
| "wall_s": wall, "rtf": wall / audio_s if audio_s else None, | |
| "ttfa_s": (t_first - t0) if t_first else None, "pieces": len(pieces), "stops": stops} | |
| if info is not None: | |
| info.update(self.last_info) | |
| def generate(self, text, lang="en", voice=None, return_info=False, **kw): | |
| """Synthesise text -> float32 waveform (24 kHz). kw: see stream().""" | |
| info = {} | |
| chunks = list(self.stream(text, lang, voice, info=info, **kw)) | |
| wav = np.concatenate(chunks).astype(np.float32) if chunks else np.zeros(0, np.float32) | |
| return (wav, info) if return_info else wav | |