Spaces:
Running
Running
| """Kyutai pocket-tts streaming TTS wrapper. | |
| API verified against pocket-tts==2.1.0: | |
| model = TTSModel.load_model() # ~6s, ~150MB | |
| state = model.get_state_for_audio_prompt("alba") # voice prompt | |
| for chunk in model.generate_audio_stream(state, text): | |
| chunk: torch.float32 1D tensor, 1920 samples at 24kHz (80ms) | |
| Voices: alba, marius, javert, jean, fantine, cosette, eponine, azelma. | |
| "alba" is a warm female English voice. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from functools import lru_cache | |
| from typing import Iterator | |
| import numpy as np | |
| DEFAULT_VOICE = os.environ.get("POCKET_TTS_VOICE", "alba") | |
| def _load_engine(): | |
| from pocket_tts import TTSModel | |
| print("[tts] Loading pocket-tts...") | |
| model = TTSModel.load_model() | |
| print(f"[tts] Loaded. sample_rate={model.sample_rate}Hz") | |
| return model | |
| def _load_state(voice_name: str): | |
| return _load_engine().get_state_for_audio_prompt(voice_name) | |
| def sample_rate() -> int: | |
| return _load_engine().sample_rate | |
| def _chunk_to_int16(chunk) -> np.ndarray: | |
| audio = chunk.detach().cpu().numpy().astype(np.float32) | |
| audio = np.clip(audio, -1.0, 1.0) | |
| return (audio * 32767.0).astype(np.int16) | |
| def synthesize_full(text: str, voice: str = DEFAULT_VOICE) -> tuple[int, np.ndarray]: | |
| """One-shot synthesis. Returns (sample_rate, int16 mono audio).""" | |
| model = _load_engine() | |
| state = _load_state(voice) | |
| chunks = [] | |
| for chunk in model.generate_audio_stream(state, text): | |
| chunks.append(_chunk_to_int16(chunk)) | |
| audio = ( | |
| np.concatenate(chunks) | |
| if chunks | |
| else np.zeros(0, dtype=np.int16) | |
| ) | |
| return model.sample_rate, audio | |
| def speak_stream(text: str, voice: str = DEFAULT_VOICE) -> Iterator[tuple[int, np.ndarray]]: | |
| """Yield (sample_rate, int16 mono) chunks as they decode (~80ms each).""" | |
| model = _load_engine() | |
| state = _load_state(voice) | |
| sr = model.sample_rate | |
| for chunk in model.generate_audio_stream(state, text): | |
| yield (sr, _chunk_to_int16(chunk)) | |
| if __name__ == "__main__": | |
| import time | |
| text = "Hi! I am Wren. Let us try the word 'red' together." | |
| t0 = time.time() | |
| for i, (sr, chunk) in enumerate(speak_stream(text)): | |
| if i == 0: | |
| print(f"first chunk after {time.time()-t0:.2f}s, sr={sr}, samples={len(chunk)}") | |
| print(f"total {time.time()-t0:.2f}s, {i+1} chunks") | |