tktts_winrt.py ダウンロード/コピー
tktts_winrt.py
tktts_winrt.py
1"""WinRT backend for tktts.
2
3このモジュールはtkttsのWinRTバックエンドを提供します。
4tktts_pyttsx3.pyと並行して、ドロップインスタイルのバックエンドとして機能することを目的としています。
5Pythonの `winsdk` パッケージを介して `Windows.Media.SpeechSynthesis` を使用します。
6
7インストール(Windowsの場合):
8 pip install winsdk
9
10注意点:
11 * WinRT SpeechSynthesizerはWAVオーディオストリームを生成します。
12 * `speak_rate` は、値が10より大きい場合、`pyttsx3` のようなWPM(Words Per Minute)として解釈されます(例: 150 -> 1.0x)。
13 値が6以下の場合は、WinRTの相対的な速度として解釈されます。
14 * ナレーターの「自然な音声」は、Windowsのビルドや音声パッケージによっては、サードパーティ製プログラムに公開されない場合があります。
15 `list_available_voices()` を使用して、WinRTが実際に認識できる音声を確認してください。
16
17関連リンク: :doc:`tktts_winrt_usage`
18"""
19
20from __future__ import annotations
21
22import asyncio
23import os
24import sys
25import tempfile
26import threading
27from typing import Any
28
29from tktts_base import apply_replacements, normalize_speaker, split_dialogue
30
31
32TTS_ENGINE_NAME = "winrt"
33DEFAULT_WINRT_VOICE = "Nanami" # fallback logic handles systems without Nanami
34DEFAULT_PYTTSX3_COMPAT_RATE = 150.0
35
36
37# -----------------------------------------------------------------------------
38# Lazy WinRT imports
39# -----------------------------------------------------------------------------
40
41def _import_winrt():
42 """WinRTクラスを遅延インポートします。
43
44 概要: WinRTの必要なクラスを動的にインポートします。
45 詳細説明: モジュールインポート時にこれらをインポートすると、非Windowsマシンではこのバックエンドが使用不能になり、またオプションの依存関係が不足している場合も早期に致命的なエラーが発生するため、必要な時までインポートを遅延させます。
46 :returns: `SpeechSynthesizer`, `Buffer`, `DataReader`, `InputStreamOptions` クラスのタプル。
47 """
48 try:
49 from winsdk.windows.media.speechsynthesis import SpeechSynthesizer
50 from winsdk.windows.storage.streams import Buffer, DataReader, InputStreamOptions
51 except ImportError as e:
52 print("エラー: tktts_winrtに必要なライブラリが不足しているか、サポートされていないプラットフォームです。")
53 print(f"詳細: {e}")
54 print(" インストール方法: pip install winsdk")
55 input("\nENTERを押して終了>>\n")
56 sys.exit(1)
57
58 return SpeechSynthesizer, Buffer, DataReader, InputStreamOptions
59
60
61def _run_async(coro):
62 """同期コードから非同期コルーチンを実行します。
63
64 概要: 非同期コルーチンを同期的に実行します。
65 詳細説明: コマンドラインでの使用には `asyncio.run()` で十分ですが、このバックエンドが既にイベントループを所有しているコードから呼び出された場合にクラッシュしないよう、スレッドでのフォールバック処理を含みます。
66 :param coro: Any: 実行する非同期コルーチン。
67 :returns: Any: コルーチンの実行結果。
68 :raises BaseException: コルーチン内で発生した例外を呼び出し元のスレッドに再スローします。
69 """
70 try:
71 asyncio.get_running_loop()
72 except RuntimeError:
73 return asyncio.run(coro)
74
75 result_box: dict[str, Any] = {}
76 error_box: dict[str, BaseException] = {}
77
78 def runner():
79 try:
80 result_box["result"] = asyncio.run(coro)
81 except BaseException as e: # noqa: BLE001 - re-raised in caller thread
82 error_box["error"] = e
83
84 th = threading.Thread(target=runner, daemon=True)
85 th.start()
86 th.join()
87
88 if "error" in error_box:
89 raise error_box["error"]
90 return result_box.get("result")
91
92
93# -----------------------------------------------------------------------------
94# Voice handling
95# -----------------------------------------------------------------------------
96
97def _safe_attr(obj: Any, name: str, default: Any = "") -> Any:
98 """オブジェクトの属性を安全に取得します。
99
100 概要: オブジェクトから指定された属性を安全に取得し、エラー発生時はデフォルト値を返します。
101 詳細説明: `getattr()` を使用して属性を取得しますが、属性が存在しない、またはアクセス中に何らかの例外が発生した場合に、指定されたデフォルト値を返します。
102 :param obj: Any: 属性を取得する対象のオブジェクト。
103 :param name: str: 取得する属性の名前。
104 :param default: Any, optional: 属性が見つからない場合やエラーが発生した場合に返すデフォルト値。デフォルトは空文字列。
105 :returns: Any: 取得された属性の値、またはデフォルト値。
106 """
107 try:
108 return getattr(obj, name)
109 except Exception:
110 return default
111
112
113def _voice_gender_text(voice: Any) -> str:
114 """WinRT音声オブジェクトから性別情報を文字列として取得します。
115
116 概要: WinRTのVoiceInformationオブジェクトから性別を判別し、文字列として返します。
117 詳細説明: `voice.gender` 属性が存在する場合、それが列挙型であればその名前を、そうでなければ直接文字列として返します。
118 :param voice: Any: WinRTの `VoiceInformation` オブジェクト。
119 :returns: str: 音声の性別を表す文字列。
120 """
121 gender = _safe_attr(voice, "gender", "")
122 if hasattr(gender, "name"):
123 return str(gender.name)
124 return str(gender)
125
126
127def _voice_to_dict(voice: Any) -> dict[str, Any]:
128 """WinRT VoiceInformationオブジェクトをpyttsx3ライクな辞書に変換します。
129
130 概要: WinRTの `VoiceInformation` オブジェクトから音声の詳細情報を抽出し、`pyttsx3` と互換性のある辞書形式で返します。
131 詳細説明: 音声の表示名、ID、言語、性別、説明を抽出し、キーと値のペアで構成される辞書として返します。
132 :param voice: Any: WinRTの `VoiceInformation` オブジェクト。
133 :returns: dict[str, Any]: 変換された音声情報辞書。
134 """
135 display_name = str(_safe_attr(voice, "display_name", ""))
136 voice_id = str(_safe_attr(voice, "id", ""))
137 language = str(_safe_attr(voice, "language", ""))
138 description = str(_safe_attr(voice, "description", ""))
139
140 return {
141 "name": display_name or voice_id,
142 "id": voice_id,
143 "lang": language,
144 "gender": _voice_gender_text(voice),
145 "description": description,
146 }
147
148
149def _voice_match_text(voice: Any) -> str:
150 """音声のマッチングに使用するテキスト文字列を生成します。
151
152 概要: 音声情報の主要な要素を結合し、検索用の一つの小文字文字列を生成します。
153 詳細説明: `_voice_to_dict` で取得した音声情報(名前、ID、言語、性別、説明)を改行で結合し、小文字に変換して返します。これにより、部分一致検索が容易になります。
154 :param voice: Any: WinRTの `VoiceInformation` オブジェクト。
155 :returns: str: 音声マッチングに使用する結合されたテキスト。
156 """
157 d = _voice_to_dict(voice)
158 return "\n".join(str(d.get(k, "")) for k in ["name", "id", "lang", "gender", "description"]).lower()
159
160
161def _select_voice(synthesizer: Any, target_voice: str | None):
162 """WinRT音声を部分一致で選択します。
163
164 概要: 指定された文字列に部分的に一致するWinRT音声を選択し、`SpeechSynthesizer` オブジェクトに設定します。
165 詳細説明: `display_name`、`id`、`language`、`gender`、`description` を含む広範な検索を実行します。
166 指定された `target_voice` が見つからない場合、`DEFAULT_WINRT_VOICE`("Nanami")や「ja-JP」などの日本語音声、またはシステムのデフォルト音声をフォールバックとして選択します。
167 :param synthesizer: SpeechSynthesizer: 音声合成に使用する `SpeechSynthesizer` オブジェクト。
168 :param target_voice: str | None: 検索対象の音声名、ID、言語などの部分文字列。Noneの場合、デフォルトの検索ロジックが適用されます。
169 :returns: Any: 選択された `VoiceInformation` オブジェクト、または見つからなかった場合はNone。
170 """
171 SpeechSynthesizer, _, _, _ = _import_winrt()
172
173 voices = list(SpeechSynthesizer.all_voices)
174 if not voices:
175 return None
176
177 target = (target_voice or "").strip().lower()
178 if target:
179 for voice in voices:
180 if target in _voice_match_text(voice):
181 synthesizer.voice = voice
182 return voice
183
184 # Prefer a Japanese voice if available. Otherwise keep the system default.
185 for fallback in [DEFAULT_WINRT_VOICE.lower(), "ja-jp", "japan"]:
186 for voice in voices:
187 if fallback in _voice_match_text(voice):
188 synthesizer.voice = voice
189 return voice
190
191 return _safe_attr(SpeechSynthesizer, "default_voice", None)
192
193
194def get_available_voices_info() -> list[dict[str, Any]] | bool:
195 """利用可能なWinRT音声情報を辞書リストとして返します。
196
197 概要: 現在システムで利用可能なWinRT音声の詳細情報を辞書のリストとして提供します。
198 詳細説明: `tktts_pyttsx3.py` で使用されている既存のバックエンドの慣例に合わせるため、WinRTの初期化やインポートに失敗した場合は `False` を返します。
199 :returns: list[dict[str, Any]] | bool: 各音声の名前、ID、言語、性別、説明を含む辞書のリスト。初期化エラーが発生した場合は `False`。
200 :raises SystemExit: `_import_winrt` 内部でWinRT関連ライブラリのインポートに失敗した場合、プログラムが終了するため、この例外は再スローされます。
201 """
202 try:
203 SpeechSynthesizer, _, _, _ = _import_winrt()
204 return [_voice_to_dict(v) for v in SpeechSynthesizer.all_voices]
205 except SystemExit:
206 raise
207 except Exception as e:
208 print(f"エラー: tktts_winrt.get_available_voices_info()での初期化エラー: {TTS_ENGINE_NAME}: {e}")
209 return False
210
211
212def get_available_voices() -> list[str] | bool:
213 """利用可能なWinRT音声の名前リストを返します。
214
215 概要: システムで利用可能なWinRT音声の名前のみのリストを返します。
216 詳細説明: `get_available_voices_info()` を呼び出し、その結果から各音声の `'name'` 属性を抽出してリストとして返します。
217 :returns: list[str] | bool: 利用可能な音声の名前のリスト。`get_available_voices_info()` が `False` を返した場合は `False`。
218 """
219 voices = get_available_voices_info()
220 if not voices:
221 return False
222 return [v["name"] for v in voices]
223
224
225def list_available_voices() -> bool:
226 """利用可能なWinRT音声の情報をコンソールに出力します。
227
228 概要: 利用可能なWinRT音声の詳細情報を整形して標準出力に表示します。
229 詳細説明: `get_available_voices_info()` を使用して音声情報を取得し、各音声の名前、言語、性別、ID、および説明(存在する場合)をユーザーフレンドリーな形式で表示します。
230 :returns: bool: 音声情報の取得と表示が成功した場合は `True`、失敗した場合は `False`。
231 """
232 print(f"=== 利用可能な {TTS_ENGINE_NAME} voices ===")
233 voices = get_available_voices_info()
234 if not voices:
235 return False
236
237 for v in voices:
238 print(f" Name: {v['name']}, Lang: {v['lang']}, Gender: {v['gender']}, ID: {v['id']}")
239 if v.get("description"):
240 print(f" Description: {v['description']}")
241
242 return True
243
244
245# -----------------------------------------------------------------------------
246# Speech synthesis
247# -----------------------------------------------------------------------------
248
249def _convert_speak_rate(speak_rate: float | int | None) -> float:
250 """pyttsx3ライクなWPMをWinRTの相対的な読み上げ速度に変換します。
251
252 概要: `pyttsx3` で一般的に使われるWPM(Words Per Minute)形式の読み上げ速度を、WinRTが要求する相対的な速度値に変換します。
253 詳細説明: WinRTの `speaking_rate` は相対値であり、1.0が通常速度、0.5が半速、6.0が6倍速です。
254 既存の `tktts` コードは `150` のようなWPM値を渡す傾向があるため、これをWinRT互換の相対値に変換します。
255 変換後の値は0.5から6.0の範囲に制限されます。
256 :param speak_rate: float | int | None: 読み上げ速度。`None` の場合、デフォルトの `1.0` (通常速度) を返します。
257 `10.0` より大きい値はWPMとして解釈され、それ以外の値はWinRTの相対速度として扱われます。
258 :returns: float: WinRT互換の相対的な読み上げ速度。
259 """
260 if speak_rate is None:
261 return 1.0
262
263 try:
264 rate = float(speak_rate)
265 except (TypeError, ValueError):
266 return 1.0
267
268 if rate > 10.0:
269 rate = rate / DEFAULT_PYTTSX3_COMPAT_RATE
270
271 return max(0.5, min(6.0, rate))
272
273
274async def _stream_to_bytes(stream: Any) -> bytes:
275 """WinRT SpeechSynthesisStreamをバイトデータとして読み込みます。
276
277 概要: WinRTの `SpeechSynthesisStream` オブジェクトから音声データをバイト配列として読み取ります。
278 詳細説明: ストリームのサイズを取得し、そのサイズのバッファを割り当てて非同期でデータを読み込みます。
279 読み取り後、ストリームとDataReaderオブジェクトはクローズされます。
280 :param stream: Any: WinRTの `SpeechSynthesisStream` オブジェクト。
281 :returns: bytes: ストリームから読み取られたWAV形式の音声データ。
282 """
283 _, Buffer, DataReader, InputStreamOptions = _import_winrt()
284
285 size = int(_safe_attr(stream, "size", 0))
286 if size <= 0:
287 return b""
288
289 # Reset to the beginning before reading.
290 try:
291 stream.seek(0)
292 except Exception:
293 pass
294
295 buffer = Buffer(size)
296 read_buffer = await stream.read_async(buffer, size, InputStreamOptions.READ_AHEAD)
297
298 length = int(_safe_attr(read_buffer, "length", 0)) or size
299 reader = DataReader.from_buffer(read_buffer)
300 data = bytearray(length)
301 reader.read_bytes(data)
302
303 try:
304 reader.close()
305 except Exception:
306 pass
307
308 return bytes(data)
309
310
311async def _synthesize_wav_bytes(text: str, target_voice: str | None, speak_rate: float | int | None) -> bytes:
312 """指定されたテキスト、音声、読み上げ速度で音声を合成し、WAV形式のバイトデータを返します。
313
314 概要: テキストを音声合成し、結果をWAV形式のバイトデータとして取得します。
315 詳細説明: `SpeechSynthesizer` オブジェクトを初期化し、指定された音声を選択し、読み上げ速度を設定します。
316 その後、テキストを非同期で合成してストリームを取得し、そのストリームからバイトデータを読み取ります。
317 処理の最後に、リソースを適切にクローズします。
318 :param text: str: 合成するテキスト文字列。
319 :param target_voice: str | None: 使用する音声の名前またはID。Noneの場合、デフォルトの選択ロジックが適用されます。
320 :param speak_rate: float | int | None: 読み上げ速度。`_convert_speak_rate` 関数によってWinRT互換の値に変換されます。
321 :returns: bytes: 合成されたWAV形式の音声データ。
322 """
323 SpeechSynthesizer, _, _, _ = _import_winrt()
324
325 text = text or ""
326 synthesizer = SpeechSynthesizer()
327 _select_voice(synthesizer, target_voice)
328
329 try:
330 synthesizer.options.speaking_rate = _convert_speak_rate(speak_rate)
331 except Exception:
332 # Older Windows builds may not support SpeakingRate.
333 pass
334
335 stream = await synthesizer.synthesize_text_to_stream_async(text)
336 try:
337 data = await _stream_to_bytes(stream)
338 finally:
339 for obj in [stream, synthesizer]:
340 try:
341 obj.close()
342 except Exception:
343 pass
344
345 return data
346
347
348def _write_wav(outfile: str, text: str, target_voice: str | None, speak_rate: float | int | None) -> str | None:
349 """合成された音声を指定されたファイルにWAV形式で保存します。
350
351 概要: テキストを音声合成し、その結果のWAVデータを指定されたパスにファイルとして書き出します。
352 詳細説明: `_synthesize_wav_bytes` を使用して音声データを取得します。
353 出力ディレクトリが存在しない場合は作成し、取得したデータをバイナリモードでファイルに書き込みます。
354 ファイルが正しく書き込まれたか、サイズが0でないかを確認し、成功した場合はファイルパスを、失敗した場合はNoneを返します。
355 :param outfile: str: 出力WAVファイルの絶対パス。
356 :param text: str: 合成するテキスト文字列。
357 :param target_voice: str | None: 使用する音声の名前またはID。
358 :param speak_rate: float | int | None: 読み上げ速度。
359 :returns: str | None: ファイルの書き込みが成功した場合は出力ファイルのパス、失敗した場合はNone。
360 """
361 data = _run_async(_synthesize_wav_bytes(text, target_voice, speak_rate))
362 if not data:
363 print(f" エラー: {TTS_ENGINE_NAME} の音声生成結果が空です")
364 return None
365
366 outdir = os.path.dirname(os.path.abspath(outfile))
367 if outdir:
368 os.makedirs(outdir, exist_ok=True)
369
370 with open(outfile, "wb") as f:
371 f.write(data)
372
373 if not os.path.exists(outfile) or os.path.getsize(outfile) <= 0:
374 print(f" エラー: ファイル [{outfile}] の出力に失敗しました")
375 return None
376
377 return outfile
378
379
380def _play_wav_file(wavfile: str) -> bool:
381 """指定されたWAVファイルを再生します(Windows環境のみ)。
382
383 概要: `winsound` モジュールを使用してWAVファイルを再生します。
384 詳細説明: この機能はWindowsプラットフォームに特化しています。Windows以外のOSで呼び出された場合、エラーメッセージを出力し、再生は実行しません。
385 :param wavfile: str: 再生するWAVファイルのパス。
386 :returns: bool: ファイルの再生が成功した場合は `True`、Windows環境でない場合は `False`。
387 """
388 if sys.platform != "win32":
389 print(f"エラー: WAV再生はWindows環境でのみ対応しています: {wavfile}")
390 return False
391
392 import winsound
393
394 winsound.PlaySound(wavfile, winsound.SND_FILENAME)
395 return True
396
397
398def speak(outfile: str | None, text: str, voice: str | None, speak_rate: float | int | None = None) -> bool | str | None:
399 """1つのテキスト文字列を合成し、再生またはWAVファイルとして保存します。
400
401 概要: 指定されたテキストを音声合成し、`outfile` の指定に応じて一時ファイルとして再生するか、指定されたWAVファイルに保存します。
402 詳細説明: パラメータは `tktts_pyttsx3.speak()` と互換性があります。
403 `outfile` が指定されている場合、WAVファイルがそのパスに書き込まれます。
404 `outfile` が空または `None` の場合、一時的なWAVファイルが生成され、`winsound` を使用して再生されます。
405 :param outfile: str | None: 出力WAVファイルのパス。Noneまたは空文字列の場合、一時ファイルとして生成し再生します。
406 :param text: str: 合成するテキスト文字列。
407 :param voice: str | None: 使用する音声の名前またはID。Noneの場合、`DEFAULT_WINRT_VOICE` が使用されます。
408 :param speak_rate: float | int | None, optional: 読み上げ速度。デフォルトはNoneで、通常速度になります。
409 :returns: bool | str | None:
410 - `outfile` が指定されずに再生が成功した場合: `True`
411 - `outfile` が指定され保存が成功した場合: 出力ファイルのパス (`str`)
412 - 失敗した場合: `False` または `None`
413 """
414 is_save_mode = bool(outfile)
415 target_voice = voice or DEFAULT_WINRT_VOICE
416
417 if is_save_mode:
418 if str(outfile).lower().endswith(".wav") is False:
419 print(" 警告: WinRTバックエンドはWAVデータを出力します。拡張子は .wav を推奨します。")
420 return _write_wav(str(outfile), text, target_voice, speak_rate)
421
422 with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as f:
423 tmpfile = f.name
424
425 try:
426 result = _write_wav(tmpfile, text, target_voice, speak_rate)
427 if not result:
428 return False
429 return _play_wav_file(tmpfile)
430 finally:
431 try:
432 os.remove(tmpfile)
433 except OSError:
434 pass
435
436
437def speak_dialogue(
438 dialogue: list[str],
439 replacements: dict[str, str],
440 target_voices: dict[str, str] | str,
441 speakers: dict[str, Any] = {},
442 speak_rate: float | int = 150,
443 temp_dir: str | None = None,
444 outfile: str | None = None,
445 ext: str = "wav",
446 cfg: Any = None
447) -> tuple[bool, list[str] | dict[Any, Any]]:
448 """対話ブロックの音声ファイルを生成します。
449
450 概要: 複数の対話ブロックに対して音声合成を行い、一時ファイルとして保存するか、結合して再生します。
451 詳細説明: この関数は `tktts_pyttsx3.speak_dialogue()` と同様の動作をします。
452 WinRTの音声合成APIが非同期ストリームベースであるため、発話ごとに合成が実行されます。
453 `outfile` が指定された場合、各対話ブロックの音声は一時ファイルとして保存され、そのファイルパスのリストが返されます。
454 `outfile` が指定されない場合、すべてのテキストが結合され、一度に再生されます。
455 WinRTバックエンドはWAV形式のみを出力するため、`ext` パラメータは常に"wav"として扱われます。
456 :param dialogue: list[str]: 処理する対話ブロックのリスト。
457 :param replacements: dict[str, str]: テキストに適用される置換ルールを定義する辞書。
458 :param target_voices: dict[str, str] | str: スピーカー名に対応する音声IDの辞書、またはすべての対話で使用する単一の音声ID文字列。
459 :param speakers: dict[str, Any], optional: スピーカーに関する追加情報を含む辞書。デフォルトは空の辞書。
460 :param speak_rate: float | int, optional: 読み上げ速度。デフォルトは150 (WPM)。
461 :param temp_dir: str | None, optional: 一時ファイルを保存するディレクトリのパス。Noneの場合、システムのデフォルト一時ディレクトリが使用されます。
462 :param outfile: str | None, optional: 最終的な出力ファイル名。このパラメータが指定されると、音声は一時ファイルに保存されます。
463 :param ext: str, optional: 生成される一時ファイルの拡張子。WinRTはWAVを生成するため、常に"wav"として扱われます。デフォルトは"wav"。
464 :param cfg: Any, optional: 設定オブジェクト。`monologue` 属性を持つ場合があります。デフォルトはNone。
465 :returns: tuple[bool, list[str] | dict[Any, Any]]:
466 - 最初の要素は処理の成否を示すブール値 (`True` または `False`)。
467 - 2番目の要素は、保存モード (`outfile` が指定された場合) では一時ファイルのパスのリスト (`list[str]`)、再生モードでは空の辞書 (`dict[Any, Any]`)。
468 """
469 is_save_mode = bool(outfile)
470 temp_dir = temp_dir or tempfile.gettempdir()
471 os.makedirs(temp_dir, exist_ok=True)
472
473 # WinRT returns WAV. Keep downstream honest even if caller passed mp3.
474 if ext.lower() != "wav":
475 print(" 警告: WinRTバックエンドはWAVを出力します。一時ファイル拡張子を wav に変更します。")
476 ext = "wav"
477
478 print()
479 print("tktts_winrt.speak_dialogue(): ")
480 print(f" 出力ファイル: {outfile}")
481 print(f" is_save_mode: {is_save_mode}")
482 print("target_voices:", target_voices)
483
484 tmpfiles = []
485 text_all = ""
486 idx = 1
487 is_monologue = bool(getattr(cfg, "monologue", False))
488
489 for i, _dialogue in enumerate(dialogue):
490 print()
491 print(f"Dialogue {i:04d}:")
492 dialogue_list = split_dialogue(
493 _dialogue,
494 target_voices,
495 speakers=speakers,
496 default_voice=DEFAULT_WINRT_VOICE,
497 is_monologue=is_monologue,
498 )
499
500 for speaker, text in dialogue_list:
501 text = apply_replacements(text, replacements)
502 if type(target_voices) is str:
503 speaker = target_voices
504
505 speaker = normalize_speaker(speaker)
506 if type(target_voices) is str:
507 target_voice = speaker
508 else:
509 target_voice = target_voices.get(speaker, DEFAULT_WINRT_VOICE)
510
511 print(f" {idx:04d}: voice={speaker} (id={target_voice}): ", end="")
512 print(text)
513 print(f"{i:04d}: {speaker}: {target_voice}: {text}")
514
515 if is_save_mode:
516 tmpfile = os.path.join(temp_dir, f"tmp_{idx:03d}.{ext}")
517 result = _write_wav(tmpfile, text, target_voice, speak_rate)
518 if not result:
519 return False, tmpfiles
520 tmpfiles.append(tmpfile)
521 else:
522 text_all += "\n" + text
523
524 idx += 1
525
526 if is_save_mode:
527 print(f"\n{TTS_ENGINE_NAME}で音声ファイルを一時ファイルに生成しました。")
528 return True, tmpfiles
529
530 print(f"\n{TTS_ENGINE_NAME}で音声ファイルを再生中...")
531 ok = speak(None, text_all, DEFAULT_WINRT_VOICE, speak_rate=speak_rate)
532 return bool(ok), {}
533
534
535# -----------------------------------------------------------------------------
536# Small standalone test CLI
537# -----------------------------------------------------------------------------
538
539if __name__ == "__main__":
540 import argparse
541
542 parser = argparse.ArgumentParser(description="WinRT TTS test utility for tktts_winrt.py")
543 parser.add_argument("--list", type=int, default=0, choices=[0, 1], help="list available voices")
544 parser.add_argument("--voice", type=str, default=DEFAULT_WINRT_VOICE, help="voice name/id/language partial match")
545 parser.add_argument("--text", type=str, default="こんにちは。これは WinRT 音声合成のテストです。", help="text to speak")
546 parser.add_argument("--outfile", type=str, default="", help="output wav file; omit to play")
547 parser.add_argument("--rate", type=float, default=150.0, help="pyttsx3-like WPM or WinRT relative rate")
548 args = parser.parse_args()
549
550 if args.list:
551 list_available_voices()
552 else:
553 speak(args.outfile, args.text, args.voice, speak_rate=args.rate)