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

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)