#!/usr/bin/env python3 """Export a pocket-tts voice state from a WAV sample into tts_voices.json. Must run with <.venv-pockettts>/bin/python (torch + pocket-tts installed there), never with the main .venv: .venv-pockettts/bin/python tools/pocket_tts_add_voice.py \\ --wav /path/to/sample.wav --name "Marius" Determines the next free index for --name by scanning tts_voices.json (an entry counts as free if it's absent, or present but its state_path file doesn't exist yet on disk), exports the voice state to models/voices/.safetensors, and registers it in tts_voices.json via src.jsonlock. """ import argparse import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent CATALOG_PATH = REPO_ROOT / "tts_voices.json" VOICES_DIR = REPO_ROOT / "models" / "voices" sys.path.insert(0, str(REPO_ROOT)) from src.credential_store import get_secret # noqa: E402 from src.jsonlock import read_locked, write_locked # noqa: E402 def _slugify(name: str) -> str: return re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") def _next_free_index(catalog: dict, name: str) -> int: n = 1 while True: entry = catalog.get(f"{name} {n}") if entry is None: return n state_path = entry.get("state_path") if state_path and not (REPO_ROOT / state_path).exists(): return n n += 1 def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--wav", required=True, help="Path to source WAV sample") parser.add_argument("--name", required=True, help="Base voice name, e.g. 'Marius'") args = parser.parse_args() wav_path = Path(args.wav).expanduser().resolve() if not wav_path.exists(): print(f"[pocket_tts_add_voice] ERROR: wav not found: {wav_path}", file=sys.stderr) return 1 import os hf_token = get_secret("hf_token") if not hf_token: print( "[pocket_tts_add_voice] ERROR: hf_token lipsește din keyring (service echo-core).", file=sys.stderr, ) return 1 os.environ["HF_TOKEN"] = hf_token from pocket_tts import TTSModel, export_model_state catalog = read_locked(str(CATALOG_PATH)) index = _next_free_index(catalog, args.name) full_name = f"{args.name} {index}" slug = _slugify(full_name) VOICES_DIR.mkdir(parents=True, exist_ok=True) export_path = VOICES_DIR / f"{slug}.safetensors" print("[pocket_tts_add_voice] Loading pocket-tts model...") model = TTSModel.load_model() print(f"[pocket_tts_add_voice] Exporting voice state from {wav_path} -> {export_path}") state = model.get_state_for_audio_prompt(audio_conditioning=str(wav_path), truncate=True) export_model_state(state, str(export_path)) def _register(data: dict) -> dict: data[full_name] = { "engine": "pockettts", "state_path": f"models/voices/{slug}.safetensors", "owner": args.name, } return data write_locked(str(CATALOG_PATH), _register) print(f"[pocket_tts_add_voice] Registered voice '{full_name}' -> {export_path}") return 0 if __name__ == "__main__": raise SystemExit(main())