| """ |
| CLI batch tool. Imports inference.py directly. No uvicorn needed. |
| |
| Usage (emotion shorthand — maps to EMOTION_TEMPLATES for speaker/language): |
| python batch.py --file script.txt --speaker Yash --language Gujarati \ |
| --emotion happy --output ./out/ [--seed 42] [--resume] |
| |
| Usage (raw description — full control): |
| python batch.py --file script.txt --speaker Yash --language Gujarati \ |
| --description "Yash speaks very fast with a very high-pitched, extremely joyful voice." --output ./out/ |
| |
| --emotion and --description are mutually exclusive. |
| """ |
| import argparse |
| import os |
| import sys |
| import json |
| from pathlib import Path |
|
|
|
|
| VALID_EMOTIONS = ["calm", "happy", "narrative", "angry", "command", "excited", "sad", "fearful"] |
| |
| |
| MEDIUM_RELIABILITY: set[str] = {"angry", "command", "excited", "sad", "fearful"} |
|
|
|
|
| def resolve_description(args) -> str: |
| if args.description: |
| return args.description |
|
|
| from app.constants import EMOTION_TEMPLATES, EMOTION_METADATA |
| templates = EMOTION_TEMPLATES.get(args.language, {}).get(args.speaker, {}) |
| template = templates.get(args.emotion) |
| if not template: |
| sys.exit( |
| f"ERROR: No template for emotion '{args.emotion}' with speaker '{args.speaker}' " |
| f"in language '{args.language}'. Use --description for a custom description." |
| ) |
| if args.emotion in MEDIUM_RELIABILITY: |
| print( |
| f"WARNING: '{args.emotion}' has medium reliability for Gujarati — " |
| f"emotion output is not officially benchmarked and may sound neutral.", |
| file=sys.stderr, |
| ) |
| return template |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--file", required=True) |
| parser.add_argument("--speaker", default="Yash") |
| parser.add_argument("--language", default="Gujarati") |
| parser.add_argument("--output", default="./out/") |
| parser.add_argument("--seed", type=int, default=42) |
| parser.add_argument("--resume", action="store_true") |
|
|
| desc_group = parser.add_mutually_exclusive_group(required=True) |
| desc_group.add_argument("--description", help="Freeform description of how to speak") |
| desc_group.add_argument( |
| "--emotion", |
| choices=VALID_EMOTIONS, |
| help="Emotion shorthand — maps to EMOTION_TEMPLATES for the given speaker/language", |
| ) |
|
|
| args = parser.parse_args() |
|
|
| from app.constants import SPEAKERS_BY_LANGUAGE |
| valid_speakers = SPEAKERS_BY_LANGUAGE.get(args.language, []) |
| if not valid_speakers: |
| sys.exit(f"ERROR: Language '{args.language}' not supported. Valid: {list(SPEAKERS_BY_LANGUAGE)}") |
| if args.speaker not in valid_speakers: |
| sys.exit(f"ERROR: Speaker '{args.speaker}' not valid for language '{args.language}'. Valid: {valid_speakers}") |
|
|
| description = resolve_description(args) |
|
|
| out_dir = Path(args.output) |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| lines = Path(args.file).read_text().strip().splitlines() |
|
|
| from app.service import tts_service |
| from app.config import settings |
| import torch |
| device = "mps" if torch.backends.mps.is_available() else "cpu" |
| print(f"Loading model on {device}...") |
| tts_service.load(settings.model_name, device) |
| print(f"Ready. Processing {len(lines)} lines...") |
|
|
| manifest = [] |
| for i, line in enumerate(lines): |
| out_path = out_dir / f"{i+1:04d}.wav" |
| if args.resume and out_path.exists(): |
| print(f" [{i+1}/{len(lines)}] skip (exists): {out_path}") |
| manifest.append({"index": i+1, "text": line, "file": str(out_path), "skipped": True}) |
| continue |
|
|
| print(f" [{i+1}/{len(lines)}] {line[:60]}...") |
| audio_bytes = tts_service.run_inference(line, description) |
|
|
| if len(audio_bytes) < 1000: |
| print(f" WARNING: output suspiciously small ({len(audio_bytes)} bytes), skipping") |
| manifest.append({"index": i+1, "text": line, "file": None, "error": "too_short"}) |
| continue |
|
|
| out_path.write_bytes(audio_bytes) |
| manifest.append({"index": i+1, "text": line, "file": str(out_path)}) |
|
|
| manifest_path = out_dir / "manifest.json" |
| manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2)) |
| print(f"Done. Manifest: {manifest_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|