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())