rupkotha / core /tts.py
Deb
rupkotha_1st_commit
f655146
Raw
History Blame Contribute Delete
3.62 kB
# core/tts.py
"""Story text β†’ grandmother-voice audio (EN/BN).
EN β†’ VoxCPM2 Voice Design persona (Build Order step 5 β€” still stubbed).
BN β†’ get_config().tts_bn_backend ('indic_parler' or 'indic_tts'), synthesised on
Modal via Indic Parler-TTS. The grandmother persona is controlled by the
caption below β€” tune it for warmth and storytelling pace (CLAUDE.md Β§9).
Returns (None, label) if TTS unavailable or failed. Never raises.
"""
import tempfile
from pathlib import Path
from core.model_config import UI_MOCK, get_config
from core.modal_infra import synthesize_bengali_remote, synthesize_english_remote
# A pre-generated sample clip for RUPKOTHA_MOCK=1, so the audio player has something
# to show during local UI dev (no Modal/GPU). None if the sample isn't present.
_MOCK_WAV = next(
(str(p) for p in [
Path("finetune/data/sample/demo_story_ai4bharat.wav"),
Path("finetune/data/sample/demo_story_output_sampled.wav"),
] if p.exists()),
None,
)
# Grandmother voice caption β€” controls the Indic Parler-TTS speaker persona.
# Tune for warmth and pace; run 5–10 samples and keep the best (CLAUDE.md Β§9).
GRANDMOTHER_CAPTION_BN = (
"Aditi speaks in the warm, tender voice of a loving elderly Bengali "
"grandmother telling a bedtime story to her beloved grandchild. Her pace is "
"very slow, gentle, and unhurried, pausing softly at every sentence. Her "
"delivery is affectionate and expressive, rising and falling with the natural "
"soothing melody of Bengali storytelling, full of warmth and calm. The "
"recording is very clear, close, and intimate, with no background noise."
)
# Grandmother Voice Design persona for VoxCPM2 (English). This is prefixed to the
# story text in parentheses by core/modal_infra.py β€” keep it short and concrete.
# Tune for warmth and a slow, storytelling pace (CLAUDE.md Β§11, build step 5).
GRANDMOTHER_VOICE_EN = (
"a warm, loving elderly grandmother reading a bedtime story, "
"slow and gentle pace, soft and soothing tone"
)
def _write_wav(audio_bytes: bytes) -> str:
"""Persist WAV bytes to a temp file and return its path."""
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
try:
tmp.write(audio_bytes)
finally:
tmp.close()
return tmp.name
def speak(
text: str,
language: str, # 'en' or 'bn'
voice: str = "grandmother",
) -> tuple[str | None, str]:
"""Returns (wav_path_or_None, tts_model_label). Never raises."""
cfg = get_config()
if UI_MOCK: # local UI dev β€” sample clip, no Modal/GPU
return _MOCK_WAV, "UI-mock"
if language == "bn":
label = cfg.tts_bn_backend
if not (text or "").strip():
return None, label
try:
audio_bytes = synthesize_bengali_remote(text, GRANDMOTHER_CAPTION_BN)
if not audio_bytes:
return None, label
return _write_wav(audio_bytes), label
except Exception as e: # noqa: BLE001 β€” never raise to the UI
print(f"[tts.py] Bengali TTS failed: {e}")
return None, label
# English path: VoxCPM2 Voice Design.
label = cfg.tts_en_backend
if not (text or "").strip():
return None, label
try:
audio_bytes = synthesize_english_remote(text, GRANDMOTHER_VOICE_EN)
if not audio_bytes:
return None, label
return _write_wav(audio_bytes), label
except Exception as e: # noqa: BLE001 β€” never raise to the UI
print(f"[tts.py] English TTS failed: {e}")
return None, label