irodori_tts_api.py ダウンロード/コピー

irodori_tts_api.py をダウンロード

irodori_tts_api.py
irodori_tts_api.py
  1#!/usr/bin/env python3
  2"""
  3概要:
  4    非常駐型Irodori-TTSのFlask API用Windows/Linuxコマンドラインクライアントです。
  5
  6詳細説明:
  7    環境変数 IRODORI_TTS_API またはコマンドライン引数で指定されたサーバーに対して
  8    HTTPリクエストを送信し、音声合成またはサーバー情報の取得を行います。
  9
 10関連リンク:
 11    irodori_tts_api_usage
 12"""
 13
 14from __future__ import annotations
 15
 16import argparse
 17import json
 18import os
 19import sys
 20import urllib.error
 21import urllib.request
 22from pathlib import Path
 23
 24
 25DEFAULT_SERVER = os.environ.get("IRODORI_TTS_API", "http://192.168.27.18:8002")
 26
 27
 28def parser() -> argparse.ArgumentParser:
 29    """
 30    概要:
 31        コマンドライン引数パーサーを構築します。
 32
 33    詳細説明:
 34        テキスト入力、サーバー設定、出力パス、音声合成の各種パラメータおよび
 35        サーバー状態確認用の引数を定義します。
 36
 37    戻り値:
 38        :returns: コマンドライン引数を解析するためのパーサーオブジェクト。
 39        :rtype: argparse.ArgumentParser
 40    """
 41    argparser = argparse.ArgumentParser(description="Irodori-TTS non-resident API client")
 42    text_group = argparser.add_mutually_exclusive_group()
 43    text_group.add_argument("--text", help="text to synthesize")
 44    text_group.add_argument("--text-file", type=Path, help="UTF-8 text file to synthesize")
 45    argparser.add_argument("--server", default=DEFAULT_SERVER, help=f"API root (default: {DEFAULT_SERVER})")
 46    argparser.add_argument("--output", type=Path, help="output WAV path")
 47    argparser.add_argument("--voice", default="none", help="registered server-side voice ID, or 'none'")
 48    argparser.add_argument("--caption", help="Irodori Voice Design caption")
 49    argparser.add_argument("--num-steps", type=int, help="sampling steps")
 50    argparser.add_argument("--duration-scale", type=float, help="duration multiplier")
 51    argparser.add_argument("--sway", action="store_true", help="use sway sampling schedule")
 52    argparser.add_argument("--sway-coeff", type=float, default=-1.0, help="sway coefficient (default: -1.0)")
 53    argparser.add_argument("--timeout", type=float, default=1800, help="HTTP timeout in seconds (default: 1800)")
 54    argparser.add_argument("--list-voices", action="store_true", help="list registered server-side voices")
 55    argparser.add_argument("--health", action="store_true", help="show server health and exit")
 56    return argparser
 57
 58
 59def request_json(url: str, timeout: float) -> dict:
 60    """
 61    概要:
 62        指定したURLに対してGETリクエストを送信し、JSONレスポンスを辞書として取得します。
 63
 64    詳細説明:
 65        urllib.requestを使用して同期的にリクエストを行い、レスポンスのUTF-8文字列をJSONとして解析します。
 66
 67    引数:
 68        :param url: リクエスト先のURL文字列。
 69        :type url: str
 70        :param timeout: タイムアウト時間(秒)。
 71        :type timeout: float
 72
 73    戻り値:
 74        :returns: 解析されたJSONデータ(辞書)。
 75        :rtype: dict
 76    """
 77    with urllib.request.urlopen(url, timeout=timeout) as response:
 78        return json.loads(response.read().decode("utf-8"))
 79
 80
 81def main() -> int:
 82    """
 83    概要:
 84        コマンドライン引数を解析し、音声合成またはサーバー状態取得のAPIリクエストを実行します。
 85
 86    詳細説明:
 87        healthやlist_voicesが指定された場合はJSONを取得してコンソールに表示します。
 88        音声合成の場合は、指定されたテキストと各種パラメータからJSONペイロードを構築し、
 89        POSTリクエストを送信して応答をWAV音声ファイルとして保存します。
 90
 91    戻り値:
 92        :returns: 実行結果を表す終了コード。成功時は0、エラー時は1。
 93        :rtype: int
 94    """
 95    args = parser().parse_args()
 96    root = args.server.rstrip("/")
 97    try:
 98        if args.health:
 99            print(json.dumps(request_json(f"{root}/health", args.timeout), ensure_ascii=False, indent=2))
100            return 0
101        if args.list_voices:
102            listing = request_json(f"{root}/v1/audio/voices", args.timeout)
103            for item in listing.get("data", []):
104                print(item.get("id", ""))
105            return 0
106        if args.text is not None:
107            text = args.text
108        elif args.text_file is not None:
109            text = args.text_file.read_text(encoding="utf-8")
110        else:
111            parser().error("one of --text or --text-file is required unless --health/--list-voices is used")
112        if args.output is None:
113            parser().error("--output is required for synthesis")
114
115        irodori: dict[str, object] = {}
116        if args.caption:
117            irodori["caption"] = args.caption
118        if args.num_steps is not None:
119            irodori["num_steps"] = args.num_steps
120        if args.duration_scale is not None:
121            irodori["duration_scale"] = args.duration_scale
122        if args.sway:
123            irodori["t_schedule_mode"] = "sway"
124            irodori["sway_coeff"] = args.sway_coeff
125        payload = json.dumps({"model": "irodori-tts", "input": text, "voice": args.voice, "response_format": "wav", "irodori": irodori}, ensure_ascii=False).encode("utf-8")
126        request_object = urllib.request.Request(f"{root}/v1/audio/speech", data=payload, headers={"Content-Type": "application/json", "Accept": "audio/wav"}, method="POST")
127        with urllib.request.urlopen(request_object, timeout=args.timeout) as response:
128            audio = response.read()
129        if not audio:
130            raise RuntimeError("server returned an empty audio response")
131        args.output.parent.mkdir(parents=True, exist_ok=True)
132        args.output.write_bytes(audio)
133        print(f"Saved: {args.output} ({len(audio):,} bytes)")
134        return 0
135    except urllib.error.HTTPError as exc:
136        detail = exc.read().decode("utf-8", errors="replace")
137        print(f"HTTP {exc.code}: {detail}", file=sys.stderr)
138        return 1
139    except (OSError, RuntimeError, urllib.error.URLError, json.JSONDecodeError) as exc:
140        print(f"Error: {exc}", file=sys.stderr)
141        return 1
142
143
144if __name__ == "__main__":
145    raise SystemExit(main())