tonights-tale / tts.py
Zhenzewu's picture
Upload folder using huggingface_hub
1e2ed65 verified
Raw
History Blame Contribute Delete
1.9 kB
"""Bedtime narration: Kokoro-82M (Apache 2.0) — an 82M-parameter TTS that fits the
hackathon's small-model spirit perfectly.
Voices: zh / en / fr supported; German has no Kokoro voice -> returns None and the
UI degrades gracefully to text-only for German stories.
"""
from __future__ import annotations
import os
from typing import Optional
import numpy as np
MOCK = os.environ.get("STORY_MOCK") == "1"
SAMPLE_RATE = 24_000
# language -> (kokoro lang_code, voice)
VOICES = {
"zh": ("z", "zf_xiaobei"),
"en": ("a", "af_heart"),
"fr": ("f", "ff_siwis"),
}
_pipelines: dict[str, object] = {}
def _get_pipeline(lang_code: str):
if lang_code not in _pipelines:
from kokoro import KPipeline
# device='cpu' is required on ZeroGPU: Kokoro's CUDA autodetect calls
# torch._C._cuda_init outside a @spaces.GPU scope, which ZeroGPU forbids.
# 82M params synthesize comfortably on CPU anyway.
_pipelines[lang_code] = KPipeline(
lang_code=lang_code, repo_id="hexgrad/Kokoro-82M", device="cpu"
)
return _pipelines[lang_code]
def synthesize(text: str, language: str) -> Optional[tuple[int, np.ndarray]]:
"""Returns (sample_rate, waveform) for gr.Audio, or None when unsupported (de)."""
if language not in VOICES:
return None
if MOCK:
return SAMPLE_RATE, np.zeros(SAMPLE_RATE // 2, dtype=np.float32)
lang_code, voice = VOICES[language]
pipeline = _get_pipeline(lang_code)
chunks: list[np.ndarray] = []
pause = np.zeros(int(SAMPLE_RATE * 0.45), dtype=np.float32) # gentle bedtime pause
for _, _, audio in pipeline(text, voice=voice):
wav = audio.numpy() if hasattr(audio, "numpy") else np.asarray(audio)
chunks.append(wav.astype(np.float32))
chunks.append(pause)
if not chunks:
return None
return SAMPLE_RATE, np.concatenate(chunks)