| |
| """Call a project Fish TTS API with a zero-shot voice or saved reference ID.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import base64 |
| from pathlib import Path |
|
|
| import requests |
|
|
|
|
| def parse_args() -> argparse.Namespace: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--url", default="http://127.0.0.1:8080/v1/tts") |
| parser.add_argument("--text", required=True) |
| voice = parser.add_mutually_exclusive_group() |
| voice.add_argument("--reference-audio", type=Path) |
| voice.add_argument("--reference-id") |
| parser.add_argument("--reference-text") |
| parser.add_argument("--output", type=Path, default=Path("output.wav")) |
| parser.add_argument("--api-key") |
| parser.add_argument("--seed", type=int) |
| parser.add_argument("--max-new-tokens", type=int, default=1024) |
| parser.add_argument("--chunk-length", type=int, default=200) |
| parser.add_argument("--top-p", type=float, default=0.9) |
| parser.add_argument("--temperature", type=float, default=0.9) |
| parser.add_argument("--repetition-penalty", type=float, default=1.1) |
| parser.add_argument( |
| "--normalize", |
| action=argparse.BooleanOptionalAction, |
| default=True, |
| help="Enable or disable API loudness normalization", |
| ) |
| parser.add_argument( |
| "--use-memory-cache", |
| choices=("on", "off"), |
| default="on", |
| help="Reuse cached reference codes/text, or force a fresh reference encode", |
| ) |
| return parser.parse_args() |
|
|
|
|
| def main() -> int: |
| args = parse_args() |
| references = [] |
| if args.reference_audio is not None: |
| if not args.reference_audio.is_file(): |
| raise SystemExit(f"Reference audio is missing: {args.reference_audio}") |
| if not args.reference_text or not args.reference_text.strip(): |
| raise SystemExit("--reference-text is required with --reference-audio") |
| references.append( |
| { |
| "audio": base64.b64encode(args.reference_audio.read_bytes()).decode( |
| "ascii" |
| ), |
| "text": args.reference_text, |
| } |
| ) |
| elif args.reference_text: |
| raise SystemExit("--reference-text requires --reference-audio") |
|
|
| payload = { |
| "text": args.text, |
| "references": references, |
| "reference_id": args.reference_id, |
| "format": "wav", |
| "streaming": False, |
| "normalize": args.normalize, |
| "max_new_tokens": args.max_new_tokens, |
| "chunk_length": args.chunk_length, |
| "top_p": args.top_p, |
| "temperature": args.temperature, |
| "repetition_penalty": args.repetition_penalty, |
| "seed": args.seed, |
| "use_memory_cache": args.use_memory_cache, |
| } |
| headers = {"Accept": "audio/wav"} |
| if args.api_key: |
| headers["Authorization"] = f"Bearer {args.api_key}" |
| response = requests.post( |
| args.url, |
| json=payload, |
| headers=headers, |
| timeout=600, |
| ) |
| if response.status_code != 200: |
| raise SystemExit( |
| f"TTS request failed ({response.status_code}): {response.text[:2000]}" |
| ) |
| content_type = response.headers.get("content-type", "") |
| if "audio/" not in content_type and not response.content.startswith(b"RIFF"): |
| raise SystemExit(f"Unexpected response content type: {content_type}") |
| args.output.parent.mkdir(parents=True, exist_ok=True) |
| args.output.write_bytes(response.content) |
| print(f"audio={args.output}") |
| print(f"bytes={len(response.content)}") |
| return 0 |
|
|
|
|
| if __name__ == "__main__": |
| raise SystemExit(main()) |
|
|