tktts.py ダウンロード/コピー
tktts.py
tktts.py
1"""tktts.py: 複数のTTS(Text-to-Speech)エンジンを統合し、テキストからの音声合成機能を提供します。
2
3さまざまなTTSバックエンド(pyttsx3, WinRT, VOICEVOXなど)を動的にロードし、
4テキストを音声に変換して再生またはファイルに保存する機能を提供します。
5話者ごとの音声設定、一時ファイルの管理、音声の結合なども行います。
6
7:doc:`tktts_usage`
8"""
9import os
10import sys
11import re
12import time
13import subprocess
14import shutil
15import tempfile
16from pathlib import Path
17import traceback
18import inspect
19
20# --- コアライブラリとオプションTTSバックエンド ---
21# 各TTSエンジン固有の依存関係は必須にしない。
22# 使えるバックエンドだけを安全に登録する。
23import importlib
24import importlib.util
25from types import SimpleNamespace
26
27CORE_LIBS = ["pydub", "pyperclip", "chardet"]
28missing_core = [name for name in CORE_LIBS if importlib.util.find_spec(name) is None]
29if missing_core:
30 print(f"Error: Missing core libraries: {', '.join(missing_core)}")
31 print(" install: pip install pydub pyperclip chardet")
32 print(" MP3などを扱う場合は FFmpeg もPATHに設定してください。")
33 raise SystemExit(1)
34
35import pyperclip
36import chardet
37from pydub import AudioSegment
38
39
40def check_ffmpeg_available() -> bool:
41 """システムにFFmpegがインストールされているかを確認します。
42
43 FFmpegとFFprobeの両方の実行可能ファイルがシステムのPATHで利用可能かをチェックし、
44 その結果をコンソールに出力します。
45
46 :returns: bool - FFmpegとFFprobeの両方が利用可能であればTrue、そうでなければFalse。
47 """
48 ffmpeg = shutil.which("ffmpeg")
49 ffprobe = shutil.which("ffprobe")
50
51 if ffmpeg is None:
52 print("❌ ffmpeg が見つかりません。")
53 return False
54
55 if ffprobe is None:
56 print("⚠️ ffprobe が見つかりません。読み込み時に問題が出る可能性があります。")
57 return False
58
59 print(f"✅ ffmpeg: {ffmpeg}")
60 print(f"✅ ffprobe: {ffprobe}")
61 return True
62
63ffmpeg_path = shutil.which("ffmpeg")
64
65if ffmpeg_path is None:
66 print("tktts.py: ffmpeg が見つかりません")
67 raise SystemExit(1)
68else:
69# print("tktts.py: ffmpeg が使えます:", ffmpeg_path)
70 pass
71
72
73def _safe_import(module_name: str, dependency: str | None = None):
74 """オプションのTTSバックエンドモジュールを安全にインポートします。
75
76 依存ライブラリがインストールされていない場合や、インポート中にエラーが発生した場合は、
77 そのモジュールをスキップし、Noneを返します。
78 これにより、一部のTTSバックエンドが利用できなくても、モジュール全体がクラッシュすることなく
79 動作を継続できます。
80
81 :param module_name: str: インポートするモジュールの名前。
82 :param dependency: str or None: ``module_name`` が依存する外部ライブラリの名前。
83 この依存ライブラリが未導入の場合はインポートを試みません。
84 :returns: module or None - 正常にインポートされたモジュール、またはインポートに失敗した場合はNone。
85 """
86 if dependency and importlib.util.find_spec(dependency) is None:
87 print(
88 f"Warning in tktts.py: {module_name} is disabled "
89 f"because optional dependency [{dependency}] is not installed."
90 )
91 return None
92
93 try:
94 return importlib.import_module(module_name)
95 except SystemExit as e:
96 print(f"Warning in tktts.py: {module_name} terminated during import: {e}")
97 return None
98 except Exception as e:
99 print(f"\nWarning in tktts.py: Import error for {module_name}")
100 print("------------------------------------------------------------------")
101 print(f"Error message: {e}")
102 traceback.print_exc()
103 print("------------------------------------------------------------------")
104 return None
105
106
107tktts_pyttsx3 = _safe_import("tktts_pyttsx3", "pyttsx3")
108tktts_winrt = _safe_import("tktts_winrt", "winsdk")
109tktts_voicevox = _safe_import("tktts_voicevox")
110tktts_qwen3 = _safe_import("tktts_qwen3", "qwen_tts")
111tktts_irodori = _safe_import("tktts_irodori", "irodori_tts")
112tktts_aquestalkplayer = _safe_import("tktts_aquestalkplayer")
113tktts_openai = _safe_import("tktts_openai", "openai")
114tktts_elevenlabs = _safe_import("tktts_elevenlabs", "elevenlabs")
115tktts_gemini = _safe_import("tktts_gemini", "google.genai")
116
117
118default_pyttsx3_voice = "Zira"
119default_winrt_voice = "Ayumi"
120default_voicevox_voice = "四国めたん"
121default_qwen3_voice = "Ono_Anna"
122default_irodori_voice = "default"
123default_aqt_preset = "れいむ"
124default_openai_voice = "alloy"
125# 旧コードとの互換性のため、綴り間違いの変数名も残す。
126default_optnai_voice = default_openai_voice
127default_elevenlabs_voice = "Rachel"
128default_gemini_voice = "Kore"
129
130TTS_ENGINES = {
131 "pyttsx3": {
132 "engine": tktts_pyttsx3,
133 "default_voice": default_pyttsx3_voice,
134 "ext": "wav",
135 "direct_playback": True,
136 },
137 "winrt": {
138 "engine": tktts_winrt,
139 "default_voice": default_winrt_voice,
140 "ext": "wav",
141 "direct_playback": True,
142 },
143 "onecore": {
144 "engine": tktts_winrt,
145 "default_voice": default_winrt_voice,
146 "ext": "wav",
147 "direct_playback": True,
148 },
149 "voicevox": {
150 "engine": tktts_voicevox,
151 "default_voice": default_voicevox_voice,
152 "ext": "wav",
153 "direct_playback": False,
154 },
155 "qwen3": {
156 "engine": tktts_qwen3,
157 "default_voice": default_qwen3_voice,
158 "ext": "wav",
159 "direct_playback": False,
160 },
161 "qwen": {
162 "engine": tktts_qwen3,
163 "default_voice": default_qwen3_voice,
164 "ext": "wav",
165 "direct_playback": False,
166 },
167 "irodori": {
168 "engine": tktts_irodori,
169 "default_voice": default_irodori_voice,
170 "ext": "wav",
171 "direct_playback": False,
172 },
173 "irodori-tts": {
174 "engine": tktts_irodori,
175 "default_voice": default_irodori_voice,
176 "ext": "wav",
177 "direct_playback": False,
178 },
179 "aquestalkplayer": {
180 "engine": tktts_aquestalkplayer,
181 "default_voice": default_aqt_preset,
182 "ext": "wav",
183 "direct_playback": False,
184 },
185 "atp": {
186 "engine": tktts_aquestalkplayer,
187 "default_voice": default_aqt_preset,
188 "ext": "wav",
189 "direct_playback": False,
190 },
191 "openai": {
192 "engine": tktts_openai,
193 "default_voice": default_openai_voice,
194 "ext": "mp3",
195 "direct_playback": False,
196 },
197 "elevenlabs": {
198 "engine": tktts_elevenlabs,
199 "default_voice": default_elevenlabs_voice,
200 "ext": "mp3",
201 "direct_playback": False,
202 },
203 "eleven": {
204 "engine": tktts_elevenlabs,
205 "default_voice": default_elevenlabs_voice,
206 "ext": "mp3",
207 "direct_playback": False,
208 },
209 "gemini": {
210 "engine": tktts_gemini,
211 "default_voice": default_gemini_voice,
212 "ext": "wav",
213 "direct_playback": False,
214 },
215}
216
217
218def get_available_engines(available_only: bool = True) -> list[str]:
219 """利用可能なTTSエンジンまたは登録されている全てのTTSエンジン名をリストとして返します。
220
221 GUIのプルダウンメニュー生成などにも利用できます。
222
223 :param available_only: bool: Trueの場合、実際にロードされて利用可能なエンジンのみを返します。
224 Falseの場合、設定ファイルに登録されている全てのエンジン名を返します。
225 :returns: list[str] - TTSエンジンの名前のリスト。
226 """
227 if not available_only:
228 return list(TTS_ENGINES.keys())
229 return [name for name, cfg in TTS_ENGINES.items() if cfg["engine"] is not None]
230
231
232def _supported_kwargs(func, kwargs: dict) -> dict:
233 """関数が受け取れるキーワード引数だけを残します。
234
235 関数のシグネチャを検査し、関数が定義している引数、または ``**kwargs`` を受け入れる場合にのみ、
236 辞書内の引数を保持します。これにより、バックエンド関数が予期しない引数を受け取ってエラーになるのを防ぎます。
237
238 :param func: Callable: キーワード引数を適用する対象の関数。
239 :param kwargs: dict: フィルタリング対象のキーワード引数の辞書。
240 :returns: dict - 関数が受け取れるキーワード引数のみを含む辞書。
241 """
242 try:
243 signature = inspect.signature(func)
244 except (TypeError, ValueError):
245 # シグネチャが取得できない場合は、Noneでない全ての引数を返す
246 return {k: v for k, v in kwargs.items() if v is not None}
247
248 # 関数が可変キーワード引数 (**kwargs) を受け入れる場合は、Noneでない全ての引数を返す
249 if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in signature.parameters.values()):
250 return {k: v for k, v in kwargs.items() if v is not None}
251
252 # 関数が定義している引数のみをフィルタリング
253 return {
254 k: v
255 for k, v in kwargs.items()
256 if v is not None and k in signature.parameters
257 }
258
259
260def _call_backend(func, **kwargs):
261 """内部的にバックエンド関数の呼び出しをラップし、サポートされている引数のみを渡します。
262
263 :param func: Callable: 呼び出すバックエンド関数。
264 :param kwargs: Any: 関数に渡すキーワード引数。
265 :returns: Any - バックエンド関数の実行結果。
266 """
267 return func(**_supported_kwargs(func, kwargs))
268
269def apply_replacements(text: str, replacements: dict[str, str] | None) -> str:
270 """置換辞書を使ってテキストを置換(大文字小文字無視)。
271
272 :param text: str: 置換処理を行う元のテキスト。
273 :param replacements: dict[str, str] or None: キーを検索パターン、値を置換後の文字列とする辞書。
274 Noneの場合、置換は行われません。
275 :returns: str - 置換処理が適用されたテキスト。
276 """
277 for key, val in (replacements or {}).items():
278 text = re.sub(key, val, text, flags=re.IGNORECASE)
279 return text
280
281def load_text(input_path: str, monologue: bool = False, wait_for_clipboard: bool = True) -> list[tuple[str | None, str]] | None:
282 """入力元('clip'またはファイルパス)からテキストを取得し、(speaker, text)リストを返します。
283
284 入力パスが"clip"の場合、クリップボードからテキストを取得します。
285 ファイルパスの場合、ファイルを読み込み、chardetでエンコーディングを検出します。
286 テキストは行ごとに解析され、カンマで区切られた話者とテキストのペア、
287 またはモノローグ形式で返されます。
288
289 :param input_path: str: テキストの入力元。"clip"またはファイルパス。
290 :param monologue: bool: Trueの場合、全てを単一の話者のテキストとして扱います。
291 Falseの場合、行を「話者,テキスト」形式で解析します。
292 :param wait_for_clipboard: bool: ``input_path`` が "clip" の場合に、
293 ユーザーがクリップボードにコピーするのを待つか。
294 :returns: list[tuple[str or None, str]] or None - (話者名, テキスト)のタプルのリスト、
295 またはエラー発生時はNone。
296 """
297
298 print()
299 print(f"load_text from {input_path}")
300 if input_path.lower() == "clip" and wait_for_clipboard:
301 print()
302 input("読み上げるテキストをクリップボードにコピーしてください:\n")
303 print("📥 クリップボードからテキストを取得中...")
304 text = pyperclip.paste()
305 text_lines = text.splitlines()
306 else:
307 file_name = input_path
308 if not os.path.isfile(file_name):
309 print(f"Error: ファイル [{file_name}] が見つかりません。")
310 return None
311
312 print(f"📖 ファイル [{file_name}] を読み込み中...")
313 try:
314 with open(file_name, "rb") as f:
315 raw_data = f.read()
316
317 result = chardet.detect(raw_data)
318 encoding = result["encoding"]
319 if encoding is None: encoding = 'utf-8'
320
321 text = raw_data.decode(encoding, errors='ignore')
322 print(f" 検出されたエンコード: {encoding}")
323 text_lines = text.splitlines()
324 except Exception as e:
325 print(f"❌ ファイル読み込み/エンコード判別エラー: {e}")
326 return None
327
328 # dialogueリストへの変換 (既存ロジックを流用)
329 dialogue = []
330 for line in text_lines:
331 line = line.strip()
332 if not line: continue
333 if line.startswith("#"): continue
334
335 if monologue:
336 dialogue.append((None, line.strip()))
337 else:
338 if "," in line:
339 try:
340 speaker, text = line.split(",", 1)
341 dialogue.append((speaker.strip(), text.strip()))
342 except ValueError:
343 continue
344 # 対話モードでカンマがない行は無視
345 # 修正前のコードではカンマがない行は無視されているため、ここでは continue
346 continue
347
348 return dialogue
349
350def create_temp_dir(temp_dir: str) -> str:
351 """指定されたパスに一時ディレクトリを作成します。
352
353 ディレクトリが存在しない場合は新しく作成します。
354
355 :param temp_dir: str: 作成する一時ディレクトリのパス。
356 :returns: str - 作成された一時ディレクトリのパス。
357 """
358 if not os.path.exists(temp_dir):
359 os.makedirs(temp_dir, exist_ok=True)
360 print(f"一時ディレクトリを作成: {temp_dir}")
361 return temp_dir
362
363def get_speaker_dict(tts_engine: str, dialogue: list[tuple[str | None, str]], voices: str | None,
364 default_voicevox_voice: str = "四国めたん", default_pyttsx3_voice: str = "Zira",
365 default_winrt_voice: str = "Ayumi", default_aqt_preset: str = "れいむ",
366 default_optnai_voice: str = "alloy", default_elevenlabs_voice: str = "Rachel",
367 default_gemini_voice: str = "Kore") -> dict[str | None, str] | None:
368 """対話データと指定された音声設定に基づいて、話者と対応する音声名をマッピングする辞書を生成します。
369
370 `voices` 引数で指定された優先順位、またはデフォルトの音声設定に基づいて話者と音声の
371 マッピングを決定します。
372
373 :param tts_engine: str: 使用するTTSエンジンの名前。
374 :param dialogue: list[tuple[str or None, str]]: (話者名, テキスト)のタプルのリスト。
375 :param voices: str or None: セミコロンで区切られた音声名の文字列。
376 :param default_voicevox_voice: str: VOICEVOXのデフォルト音声名。
377 :param default_pyttsx3_voice: str: pyttsx3のデフォルト音声名。
378 :param default_winrt_voice: str: WinRTのデフォルト音声名。
379 :param default_aqt_preset: str: AquesTalkPlayerのデフォルトプリセット名。
380 :param default_optnai_voice: str: OpenAIのデフォルト音声名。
381 :param default_elevenlabs_voice: str: ElevenLabsのデフォルト音声名。
382 :param default_gemini_voice: str: Geminiのデフォルト音声名。
383 :returns: dict[str or None, str] or None - 話者名をキー、音声名を値とする辞書、
384 または無効なエンジン名の場合はNone。
385 """
386
387 tts_engine = str(tts_engine).lower()
388 if tts_engine in TTS_ENGINES:
389 default_voice = TTS_ENGINES[tts_engine]["default_voice"]
390 else:
391 print(f"\nError in tktts.get_speaker_dict(): Invalid tts engine [{tts_engine}]\n")
392 return None
393
394 print("\n話者リスト作成...")
395 voices_specified = [v.strip() for v in str(voices or "").split(";") if v.strip()]
396 if voices_specified:
397 default_voice = voices_specified[0]
398
399 speaker_names = []
400 target_voices = {}
401 for speaker, _text in dialogue or []:
402 if speaker is None or speaker == "" or speaker in speaker_names:
403 continue
404 speaker_names.append(speaker)
405 index = len(speaker_names) - 1
406 target_voices[speaker] = (
407 voices_specified[index] if index < len(voices_specified) else default_voice
408 )
409
410 target_voices[""] = default_voice
411 target_voices[None] = default_voice
412
413 for i, (speaker, voice) in enumerate(target_voices.items()):
414 print(f" {i:02d}: {speaker} => {voice}")
415
416 return target_voices
417
418def get_tts(tts_engine: str | None):
419 """指定されたTTSエンジンのモジュールを返します。
420
421 エンジン名が ``TTS_ENGINES`` に登録されていない場合や、エンジンのモジュールが
422 ロードされていない場合はNoneを返します。
423
424 :param tts_engine: str or None: 取得するTTSエンジンの名前。
425 :returns: module or None - TTSエンジンのモジュール、または利用できない場合はNone。
426 """
427 tts_engine = str(tts_engine or "").lower()
428 config = TTS_ENGINES.get(tts_engine)
429 if config is None:
430 print(f"\nError in tktts.get_tts(): Invalid tts engine [{tts_engine}]\n")
431 return None
432
433 tts_module = config["engine"]
434 if tts_module is not None:
435 return tts_module
436
437 print(f"\nError in tktts.get_tts(): TTS engine [{tts_engine}] is unavailable.")
438 print("対応バックエンドファイルと、そのオプション依存ライブラリを確認してください。")
439 return None
440
441
442def get_available_voices_info(tts_engine: str | None, endpoint: str | None = None, api_key: str | None = None):
443 """指定されたTTSエンジンで利用可能な音声の詳細情報を取得します。
444
445 バックエンドモジュールに ``get_available_voices_info`` 関数が存在する場合にそれを呼び出します。
446
447 :param tts_engine: str or None: 情報を取得するTTSエンジンの名前。
448 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
449 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
450 :returns: Any or None - 利用可能な音声の詳細情報、または機能が存在しない場合はNone。
451 """
452 tts_engine = str(tts_engine or "").lower()
453 tts = get_tts(tts_engine)
454 if tts is None or not hasattr(tts, "get_available_voices_info"):
455 return None
456 return _call_backend(
457 tts.get_available_voices_info,
458 endpoint=endpoint,
459 api_key=api_key,
460 )
461
462
463def get_available_voices(tts_engine: str | None, endpoint: str | None = None, api_key: str | None = None):
464 """指定されたTTSエンジンで利用可能な音声のリストを取得します。
465
466 バックエンドモジュールに ``get_available_voices`` 関数が存在する場合にそれを呼び出します。
467
468 :param tts_engine: str or None: リストを取得するTTSエンジンの名前。
469 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
470 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
471 :returns: list[str] or None - 利用可能な音声のリスト、または機能が存在しない場合はNone。
472 """
473 tts_engine = str(tts_engine or "").lower()
474 tts = get_tts(tts_engine)
475 if tts is None or not hasattr(tts, "get_available_voices"):
476 return None
477 return _call_backend(
478 tts.get_available_voices,
479 endpoint=endpoint,
480 api_key=api_key,
481 )
482
483
484def list_available_voices(tts_engine: str | None, endpoint: str | None = None, api_key: str | None = None) -> bool:
485 """指定されたTTSエンジンで利用可能な音声をコンソールに出力します。
486
487 バックエンドモジュールに ``list_available_voices`` 関数が存在する場合にそれを呼び出します。
488
489 :param tts_engine: str or None: リストを出力するTTSエンジンの名前。
490 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
491 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
492 :returns: bool - 処理が成功した場合はTrue、失敗した場合はFalse。
493 """
494 tts_engine = str(tts_engine or "").lower()
495 tts = get_tts(tts_engine)
496 if tts is None:
497 return False
498 if not hasattr(tts, "list_available_voices"):
499 print(f"Error: TTS module [{tts_engine}] has no list_available_voices function.")
500 return False
501
502 try:
503 ret = _call_backend(
504 tts.list_available_voices,
505 endpoint=endpoint,
506 api_key=api_key,
507 )
508 except Exception as e:
509 print(f"Error calling list_available_voices for {tts_engine}: {e}")
510 traceback.print_exc()
511 ret = False
512
513 print("===============================")
514 return ret
515
516def normalize_speaker(speaker: str | None, tts_engine: str | None = None) -> str | None:
517 """話者ラベルを正規化します。
518
519 半角スペースでは分割しない。WinRTなどの音声表示名には
520 ``Microsoft Ayumi ...`` のように空白が含まれるためである。
521 明示的なスタイル表記 ``話者(style)`` / ``話者(style)`` のみ除く。
522
523 :param speaker: str or None: 正規化する話者名。
524 :param tts_engine: str or None: 現在は使用されていないが将来的な拡張のために残されているTTSエンジン名。
525 :returns: str or None - 正規化された話者名、または入力がNoneの場合はNone。
526 """
527 if speaker is None:
528 return None
529
530 normalized = str(speaker).strip()
531 for opening in ("(", "("):
532 if opening in normalized:
533 normalized = normalized.split(opening, 1)[0].rstrip()
534 return normalized
535
536def parse_kv_string(kv_string: str | None, keys: list[str] = [], allow_no_key_kv_string: bool = False) -> dict[str | int, str]:
537 """key=val;key=val 形式を dict に変換します。
538
539 `keys` 引数で指定されたキーのリストが与えられた場合、キーが指定されていない値に対しては、
540 そのリストのインデックスに基づいてキーを割り当てます。
541
542 :param kv_string: str or None: "key=val;key=val" 形式の文字列。
543 :param keys: list[str]: キーがない値に割り当てるためのキーのリスト。
544 :param allow_no_key_kv_string: bool: キーが指定されていないアイテム(例: "value1;value2")を許可するかどうか。
545 :returns: dict[str or int, str] - 変換された辞書。キーは文字列またはインデックス。
546 """
547
548 if not kv_string: return {}
549 nkeys = len(keys)
550
551 d = {}
552 idx = 0
553 for item in kv_string.split(";"):
554 if "=" in item:
555 k, v = item.split("=", 1)
556 d[k.strip()] = v.strip()
557 elif allow_no_key_kv_string:
558 if "*" in item or item.strip() == "": continue
559
560 if idx < nkeys: d[keys[idx]] = item
561 d[idx] = item
562 idx += 1
563 for speaker in keys:
564 if speaker in d.keys(): continue
565 d[speaker] = speaker
566
567 return d
568
569def _get_attr(args: object | None, name: str, default: Any | None = None) -> Any | None:
570 """オブジェクトから属性を安全に取得します。
571
572 `args` がNoneの場合や、指定された属性が存在しない場合はデフォルト値を返します。
573
574 :param args: object or None: 属性を取得する対象のオブジェクト(通常は ``argparse.Namespace``)。
575 :param name: str: 取得する属性の名前。
576 :param default: Any or None: 属性が存在しない場合に返すデフォルト値。
577 :returns: Any or None - 取得した属性の値、またはデフォルト値。
578 """
579 return getattr(args, name, default) if args is not None else default
580
581
582def _effective_pyttsx3_rate(args: object) -> int:
583 """pyttsx3の音声速度を計算します。
584
585 ``--speak_rate`` と ``--fspeak_rate`` オプションを考慮して
586 最終的な音声速度(WPM: Words Per Minute)を決定します。
587
588 :param args: object: ``speak_rate`` と ``fspeak_rate`` 属性を持つオブジェクト。
589 :returns: int - pyttsx3に設定する有効な音声速度。
590 """
591 base_rate = float(_get_attr(args, "speak_rate", 150) or 150)
592 multiplier = _get_attr(args, "fspeak_rate", None)
593 if multiplier is not None:
594 try:
595 base_rate *= float(multiplier)
596 except (TypeError, ValueError):
597 pass
598 return max(1, int(round(base_rate)))
599
600
601def _effective_winrt_rate(args: object) -> float | int:
602 """WinRTの音声速度を計算します。
603
604 ``--fspeak_rate`` または ``--speak_rate`` オプションから音声速度を決定します。
605 GUIの `fspeak_rate` は相対倍率なので、WinRTにはそのまま渡すことができます。
606
607 :param args: object: ``fspeak_rate`` と ``speak_rate`` 属性を持つオブジェクト。
608 :returns: float or int - WinRTに設定する有効な音声速度。
609 """
610 # GUIの fspeak_rate は相対倍率なので、WinRTにはそのまま渡せる。
611 relative_rate = _get_attr(args, "fspeak_rate", None)
612 if relative_rate is not None:
613 return relative_rate
614 return _get_attr(args, "speak_rate", 150)
615
616
617def _elevenlabs_voice_settings(args: object) -> dict | None:
618 """ElevenLabsの音声設定 (``VoiceSettings``) を ``args`` オブジェクトから構築します。
619
620 ``elevenlabs_voice_settings`` または ``voice_settings`` 属性から設定辞書を取得するか、
621 個別の ``elevenlabs_stability``、``elevenlabs_similarity_boost`` などの属性から設定を組み立てます。
622
623 :param args: object: ElevenLabs関連の設定属性を持つオブジェクト。
624 :returns: dict or None - ElevenLabsの音声設定辞書、または設定がない場合はNone。
625 """
626 settings = _get_attr(args, "elevenlabs_voice_settings", None)
627 if settings is None:
628 settings = _get_attr(args, "voice_settings", None)
629 if isinstance(settings, dict):
630 return dict(settings)
631
632 values = {}
633 for key in ("stability", "similarity_boost", "style", "use_speaker_boost"):
634 value = _get_attr(args, f"elevenlabs_{key}", None)
635 if value is not None:
636 values[key] = value
637 return values or None
638
639
640def _infer_output_format(outfile: str | Path, requested_format: str | None, default_format: str) -> str:
641 """出力ファイル名、要求された形式、デフォルト形式から最終的な出力形式を推測します。
642
643 ``requested_format`` があればそれを優先し、そうでなければ ``outfile`` の拡張子から推測します。
644 どちらもなければ ``default_format`` を使用します。
645
646 :param outfile: str or Path: 出力ファイルのパス。
647 :param requested_format: str or None: 要求された出力形式(例: "mp3", "wav")。
648 :param default_format: str: デフォルトの出力形式。
649 :returns: str - 推測された出力形式(拡張子のみ、例: "mp3")。
650 """
651 if requested_format:
652 return str(requested_format).lower().lstrip(".")
653 suffix = Path(str(outfile)).suffix.lower().lstrip(".")
654 if suffix:
655 return "wav" if suffix == "wave" else suffix
656 return default_format
657
658
659def speak_dialogue(args: object, dialogue: list[tuple[str | None, str]], voice_map: dict[str | None, str] | str | None = None, speakers: dict | None = None, replacements: dict[str, str] | None = None,
660 default_voicevox_voice: str = "四国めたん", default_pyttsx3_voice: str = "Zira",
661 default_winrt_voice: str = "Ayumi", default_aqt_preset: str = "れいむ",
662 default_optnai_voice: str = "alloy", default_elevenlabs_voice: str = "Rachel",
663 default_gemini_voice: str = "Kore",
664 endpoint: str | None = None, api_key: str | None = None, output_format: str | None = None) -> str | bool:
665 """選択されたエンジンで音声生成・結合・保存または直接再生を行います。
666
667 TTSエンジンを選択し、設定を読み込みます。
668 話者ごとの音声マッピングを決定し、一時ディレクトリを作成して各セリフの音声ファイルを生成します。
669 生成された音声ファイルを結合し、指定された出力形式で保存します。
670 pyttsx3やWinRTのように直接再生に対応している場合は、保存せずに再生を試みます。
671
672 :param args: object: コマンドライン引数などの設定オブジェクト。
673 :param dialogue: list[tuple[str or None, str]]: (話者名, テキスト)のタプルのリスト。
674 :param voice_map: dict[str or None, str] or str or None: 話者と音声名をマッピングする辞書、
675 または単一の音声名文字列。
676 Noneの場合、引数 ``voices`` から生成されます。
677 :param speakers: dict or None: 以前に解析された話者情報。
678 :param replacements: dict[str, str] or None: テキスト置換ルール。
679 :param default_voicevox_voice: str: VOICEVOXのデフォルト音声名。
680 :param default_pyttsx3_voice: str: pyttsx3のデフォルト音声名。
681 :param default_winrt_voice: str: WinRTのデフォルト音声名。
682 :param default_aqt_preset: str: AquesTalkPlayerのデフォルトプリセット名。
683 :param default_optnai_voice: str: OpenAIのデフォルト音声名。
684 :param default_elevenlabs_voice: str: ElevenLabsのデフォルト音声名。
685 :param default_gemini_voice: str: Geminiのデフォルト音声名。
686 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
687 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
688 :param output_format: str or None: 出力音声ファイルの形式(例: "mp3", "wav")。
689 :returns: str or bool - 保存されたファイルのパス(str)、
690 または成功の場合はTrue (直接再生時)、失敗の場合はFalse。
691 """
692
693 print("tktts.speak_dialogue(): Generate audio files:")
694 tts_engine = str(_get_attr(args, "tts", "")).lower()
695 tts = get_tts(tts_engine)
696 if tts is None:
697 return False
698
699 config = TTS_ENGINES.get(tts_engine)
700 if config is None:
701 return False
702
703 outfile = _get_attr(args, "outfile", "") or ""
704 is_save_mode = bool(outfile)
705 monologue = bool(_get_attr(args, "monologue", False))
706 speak_rate = _get_attr(args, "speak_rate", 150)
707 tinterval = float(_get_attr(args, "tinterval", 0.5) or 0.0)
708 voices = _get_attr(args, "voices", "") or ""
709 speakers = speakers or {}
710 replacements = replacements or {}
711
712 print("\nspeak_dialogue:")
713 print(
714 f" tts_engine : {tts_engine} is monologue: {monologue} "
715 f"speak rate: {speak_rate} tinterval: {tinterval}"
716 )
717 print(f" voices : {voices}")
718 print(f" outfile : {outfile}" if is_save_mode else " is_save_mode: False")
719
720 if voice_map is None or isinstance(voice_map, str):
721 # GUIなどから単一の音声名が文字列で渡された場合も、話者マップへ
722 # 変換してバックエンドへ渡す。WinRTバックエンド側で音声名を
723 # speakerとして正規化すると、空白を含む表示名が切れる実装との
724 # 互換問題を避けられる。
725 selected_voices = voice_map if isinstance(voice_map, str) else voices
726 target_voices = get_speaker_dict(
727 tts_engine,
728 dialogue,
729 selected_voices,
730 default_voicevox_voice=default_voicevox_voice,
731 default_pyttsx3_voice=default_pyttsx3_voice,
732 default_winrt_voice=default_winrt_voice,
733 default_aqt_preset=default_aqt_preset,
734 default_optnai_voice=default_optnai_voice,
735 default_elevenlabs_voice=default_elevenlabs_voice,
736 default_gemini_voice=default_gemini_voice,
737 )
738 else:
739 target_voices = voice_map
740
741 if target_voices is None:
742 return False
743
744 if not is_save_mode and not config.get("direct_playback", False):
745 print("❌ このエンジンではファイル保存が必要です。--outfile を指定してください。")
746 return False
747
748 temp_dir_name = _get_attr(args, "temp_dir", None)
749 if not temp_dir_name:
750 print("❌ 一時ファイル保存先が必要です。--temp_dir を指定してください。")
751 return False
752 temp_dir = create_temp_dir(temp_dir_name)
753
754 call_kwargs = {
755 "dialogue": dialogue or [],
756 "replacements": replacements,
757 "target_voices": target_voices,
758 "speakers": speakers,
759 "temp_dir": temp_dir,
760 "outfile": outfile,
761 "ext": config["ext"],
762 "cfg": args,
763 }
764
765 if tts_engine == "pyttsx3":
766 call_kwargs["speak_rate"] = _effective_pyttsx3_rate(args)
767 elif tts_engine in ("winrt", "onecore"):
768 call_kwargs["speak_rate"] = _effective_winrt_rate(args)
769 elif tts_engine == "voicevox":
770 call_kwargs["endpoint"] = endpoint or _get_attr(args, "endpoint", None)
771 elif tts_engine in ("qwen3", "qwen"):
772 call_kwargs.update({
773 "language": _get_attr(args, "qwen3_language", None),
774 "model_id": _get_attr(args, "qwen3_model_id", None),
775 "device": _get_attr(args, "qwen3_device", None),
776 "dtype": _get_attr(args, "qwen3_dtype", None),
777 })
778 elif tts_engine in ("irodori", "irodori-tts"):
779 call_kwargs.update({
780 "caption": _get_attr(args, "irodori_caption", None),
781 "ref_wav": _get_attr(args, "irodori_ref_wav", None),
782 "ref_wavs": _get_attr(args, "irodori_ref_wavs", None),
783 "model_id": _get_attr(args, "irodori_model_id", None),
784 "device": _get_attr(args, "irodori_device", None),
785 "precision": _get_attr(args, "irodori_precision", None),
786 })
787 elif tts_engine in ("aquestalkplayer", "atp"):
788 call_kwargs["aquestalk_path"] = _get_attr(args, "aquestalk_path", None)
789 elif tts_engine == "openai":
790 call_kwargs["instruction"] = _get_attr(args, "instruction", None)
791 elif tts_engine == "gemini":
792 call_kwargs["instruction"] = _get_attr(args, "instruction", None)
793 elif tts_engine in ("elevenlabs", "eleven"):
794 call_kwargs.update({
795 "api_key": (
796 api_key
797 or _get_attr(args, "elevenlabs_api_key", None)
798 or _get_attr(args, "api_key", None)
799 ),
800 "model_id": (
801 _get_attr(args, "elevenlabs_model_id", None)
802 or _get_attr(args, "model_id", None)
803 ),
804 "voice_settings": _elevenlabs_voice_settings(args),
805 "output_format": _get_attr(args, "elevenlabs_output_format", None),
806 "optimize_streaming_latency": _get_attr(
807 args, "elevenlabs_optimize_streaming_latency", None
808 ),
809 "language_code": _get_attr(args, "elevenlabs_language_code", None),
810 })
811
812 print(f"\n⚙️ {tts_engine.upper()}で音声ファイルを生成中...")
813 try:
814 result = _call_backend(tts.speak_dialogue, **call_kwargs)
815 success, tmpfiles = result
816 except Exception as e:
817 print(f"❌ {tts_engine} の speak_dialogue 呼び出しエラー: {e}")
818 traceback.print_exc()
819 return False
820
821 if not success:
822 return False
823
824 # pyttsx3 / WinRT の直接再生はバックエンド側で完了する。
825 if not is_save_mode:
826 return True
827
828 tmpfiles = list(tmpfiles or [])
829 if not tmpfiles:
830 if os.path.isfile(outfile) and os.path.getsize(outfile) > 0:
831 return outfile
832 print("❌ 結合対象の一時音声ファイルが生成されませんでした。")
833 return False
834
835 final_format = _infer_output_format(outfile, output_format, config["ext"])
836 output_parent = os.path.dirname(os.path.abspath(outfile))
837 if output_parent:
838 os.makedirs(output_parent, exist_ok=True)
839
840 print(" ファイルを結合中...")
841 combined_audio = AudioSegment.silent(duration=0)
842 try:
843 for tmpfile in tmpfiles:
844 segment = AudioSegment.from_file(tmpfile, format=config["ext"])
845 combined_audio += segment
846 if tinterval > 0:
847 print(f" insert {tinterval} sec interval")
848 combined_audio += AudioSegment.silent(duration=int(tinterval * 1000))
849
850 combined_audio.export(outfile, format=final_format)
851 print(f"✅ 出力音声を {outfile} に保存しました (形式: {final_format})。")
852 except Exception as e:
853 print(f"❌ 音声ファイルの結合または保存に失敗しました: {e}")
854 traceback.print_exc()
855 return False
856 finally:
857 time.sleep(0.1)
858 print("🗑️ 一時ファイルを削除中...")
859 for filename in tmpfiles:
860 try:
861 if os.path.isfile(filename):
862 os.remove(filename)
863 except OSError as e:
864 print(f" [warn] 一時ファイルを削除できません: {filename}: {e}")
865 try:
866 if os.path.isdir(temp_dir) and not os.listdir(temp_dir):
867 os.rmdir(temp_dir)
868 except OSError:
869 pass
870
871 return outfile
872
873
874class tkTTS:
875 """複数のTTS(Text-to-Speech)エンジンを統一的に扱うためのラッパークラスです。
876
877 選択されたTTSエンジンの設定を管理し、音声合成、利用可能な音声の取得、
878 話者マッピングなどの機能を提供します。
879 """
880 def __init__(self, tts_name: str | None = None, config: SimpleNamespace | None = None):
881 """``tkTTS`` クラスのコンストラクタです。
882
883 :param tts_name: str or None: 初期化時に設定するTTSエンジンの名前。
884 :param config: SimpleNamespace or None: 設定情報を含むオブジェクト。
885 (例: ``argparse.Namespace`` のインスタンス)。
886 """
887 self.tts_name = tts_name
888 self.tts = None
889 self.config = config if config is not None else SimpleNamespace()
890 self.endpoint = getattr(self.config, "endpoint", None)
891 self.aquestalk_path = getattr(self.config, "aquestalk_path", None)
892 self.api_key = (
893 getattr(self.config, "elevenlabs_api_key", None)
894 or getattr(self.config, "api_key", None)
895 )
896
897 if tts_name is not None:
898 self.set_engine(tts_name)
899
900 def set_engine(self, tts_name: str):
901 """使用するTTSエンジンを設定します。
902
903 :param tts_name: str: 設定するTTSエンジンの名前。
904 """
905 self.tts_name = str(tts_name).lower()
906 self.tts = get_tts(self.tts_name)
907
908 def set_endpoint(self, endpoint: str):
909 """APIベースのTTSエンジンのエンドポイントURLを設定します。
910
911 :param endpoint: str: 設定するエンドポイントURL。
912 """
913 self.endpoint = endpoint
914 setattr(self.config, "endpoint", endpoint)
915
916 def set_aquestalk_path(self, aquestalk_path: str):
917 """AquesTalkPlayerの実行パスを設定します。
918
919 :param aquestalk_path: str: AquesTalkPlayerの実行可能ファイルのパス。
920 """
921 self.aquestalk_path = aquestalk_path
922 setattr(self.config, "aquestalk_path", aquestalk_path)
923
924 def set_api_key(self, api_key: str):
925 """APIベースのTTSエンジンのAPIキーを設定します。
926
927 :param api_key: str: 設定するAPIキー。
928 """
929 self.api_key = api_key
930 setattr(self.config, "elevenlabs_api_key", api_key)
931
932 def get_tts_name(self, tts_name: str | None = None) -> str:
933 """現在設定されているTTSエンジン名、または引数で指定されたTTSエンジン名を返します。
934
935 :param tts_name: str or None: 取得したいTTSエンジンの名前。
936 指定しない場合はインスタンスの設定を使用します。
937 :returns: str - TTSエンジンの名前。
938 """
939 if tts_name is None: return self.tts_name
940 return tts_name
941
942 def get_default_voice(self, tts_name: str | None = None) -> str | None:
943 """指定されたTTSエンジンのデフォルト音声名を返します。
944
945 :param tts_name: str or None: デフォルト音声名を取得するTTSエンジンの名前。
946 指定しない場合はインスタンスの設定を使用します。
947 :returns: str or None - デフォルト音声名、または設定がない場合はNone。
948 """
949 tts_def = TTS_ENGINES.get(self.get_tts_name(tts_name), None)
950 if tts_def:return tts_def["default_voice"]
951 return None
952
953 def normalize_speaker(self, speaker: str | None, tts_name: str | None = None) -> str | None:
954 """話者ラベルを正規化します。
955
956 このメソッドは、グローバル関数 ``normalize_speaker`` のラッパーです。
957
958 :param speaker: str or None: 正規化する話者名。
959 :param tts_name: str or None: 現在は使用されていないが将来的な拡張のために残されているTTSエンジン名。
960 :returns: str or None - 正規化された話者名、または入力がNoneの場合はNone。
961 """
962 if tts_name is None: tts_name = self.tts_name
963 return normalize_speaker(speaker, tts_name)
964
965 def get_available_voices_info(self, tts_name: str | None = None, endpoint: str | None = None, api_key: str | None = None):
966 """現在設定されている、または指定されたTTSエンジンで利用可能な音声の詳細情報を取得します。
967
968 このメソッドは、グローバル関数 ``get_available_voices_info`` のラッパーです。
969 インスタンスに設定されているエンドポイントやAPIキーを自動的に渡します。
970
971 :param tts_name: str or None: 情報を取得するTTSエンジンの名前。
972 指定しない場合はインスタンスの設定を使用します。
973 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
974 指定しない場合はインスタンスの設定を使用します。
975 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
976 指定しない場合はインスタンスの設定を使用します。
977 :returns: Any or None - 利用可能な音声の詳細情報、または機能が存在しない場合はNone。
978 """
979 if tts_name is None:
980 tts_name = self.tts_name
981 if endpoint is None:
982 endpoint = self.endpoint
983 if api_key is None:
984 api_key = self.api_key
985 return get_available_voices_info(tts_name, endpoint=endpoint, api_key=api_key)
986
987 def get_available_voices(self, tts_name: str | None = None, endpoint: str | None = None, api_key: str | None = None):
988 """現在設定されている、または指定されたTTSエンジンで利用可能な音声のリストを取得します。
989
990 このメソッドは、グローバル関数 ``get_available_voices`` のラッパーです。
991 インスタンスに設定されているエンドポイントやAPIキーを自動的に渡します。
992
993 :param tts_name: str or None: リストを取得するTTSエンジンの名前。
994 指定しない場合はインスタンスの設定を使用します。
995 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
996 指定しない場合はインスタンスの設定を使用します。
997 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
998 指定しない場合はインスタンスの設定を使用します。
999 :returns: list[str] or None - 利用可能な音声のリスト、または機能が存在しない場合はNone。
1000 """
1001 if tts_name is None:
1002 tts_name = self.tts_name
1003 if endpoint is None:
1004 endpoint = self.endpoint
1005 if api_key is None:
1006 api_key = self.api_key
1007 return get_available_voices(tts_name, endpoint=endpoint, api_key=api_key)
1008
1009 def list_available_voices(self, tts_name: str | None = None, endpoint: str | None = None, api_key: str | None = None) -> bool:
1010 """現在設定されている、または指定されたTTSエンジンで利用可能な音声をコンソールに出力します。
1011
1012 このメソッドは、グローバル関数 ``list_available_voices`` のラッパーです。
1013 インスタンスに設定されているエンドポイントやAPIキーを自動的に渡します。
1014
1015 :param tts_name: str or None: リストを出力するTTSエンジンの名前。
1016 指定しない場合はインスタンスの設定を使用します。
1017 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
1018 指定しない場合はインスタンスの設定を使用します。
1019 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
1020 指定しない場合はインスタンスの設定を使用します。
1021 :returns: bool - 処理が成功した場合はTrue、失敗した場合はFalse。
1022 """
1023 if tts_name is None:
1024 tts_name = self.tts_name
1025 if endpoint is None:
1026 endpoint = self.endpoint
1027 if api_key is None:
1028 api_key = self.api_key
1029 return list_available_voices(tts_name, endpoint=endpoint, api_key=api_key)
1030
1031 def show_voice_map(self, infile: str, voices: str, VOICE_MAPS: dict, is_monologue: bool, tts_name: str | None = None, endpoint: str | None = None) -> bool:
1032 """入力ファイルから話者を抽出し、既存の音声マップと指定された音声設定に基づいて、
1033 最終的な話者と音声のマッピングを表示します。
1034
1035 このメソッドは主にデバッグや設定確認のために使用され、読み上げは行いません。
1036
1037 :param infile: str: 解析する入力ファイル(テキストまたはクリップボード)。
1038 :param voices: str: セミコロンで区切られた音声指定文字列(例: "speaker1=voiceA;speaker2=voiceB;voiceC")。
1039 :param VOICE_MAPS: dict: 全てのTTSエンジンに対する既存の音声マッピング辞書。
1040 :param is_monologue: bool: 入力ファイルが独話形式か。
1041 :param tts_name: str or None: 使用するTTSエンジンの名前。指定しない場合はインスタンスの設定を使用します。
1042 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
1043 :returns: bool - 処理が成功した場合はTrue、失敗した場合はFalse。
1044 """
1045 print()
1046 print(f"[{infile}]を解析します:")
1047 dialogue = self.load_text(infile, is_monologue, wait_for_clipboard = False)
1048 if not dialogue:
1049 print("エラー: 有効なテキストデータが取得できませんでした。")
1050 if not is_monologue:
1051 print(" 対話形式でない場合は --monologue=1 オプションをつけてください。")
1052 return False
1053
1054 speakers_in_file = self.get_speakers_from_dialogue(dialogue)
1055 print(f" Speakers in [{infile}]")
1056 for idx, sp in enumerate(speakers_in_file):
1057 print(f" {idx:02d}: {sp}")
1058
1059 current_voice_map = self.update_voice_map(voice_map = VOICE_MAPS,
1060 voices = voices, speakers = speakers_in_file)
1061
1062 print()
1063 print(f"Voice map:")
1064 print(f" {'Speaker':<20} => Voice")
1065 for key, val in current_voice_map.items():
1066 if type(key) is str:
1067 print(f" {key:<20} => {val}")
1068 for key, val in current_voice_map.items():
1069 if type(key) is not str and type(key) is not int:
1070 if key is None: key = 'None'
1071 print(f" {key:<20} => {val}")
1072 for key, val in current_voice_map.items():
1073 if type(key) is int:
1074 print(f" {key:<20} => {val}")
1075
1076 print()
1077 print(f"[{infile}] から検出された話者:")
1078 for s in sorted(speakers_in_file):
1079 if s is None or s == "":
1080 voice = current_voice_map.get(s, None)
1081 if voice is None: voice = current_voice_map.get(0, None)
1082 print(f" (独話): {voice}")
1083 else:
1084 ns = self.normalize_speaker(s)
1085 voice = current_voice_map.get(ns, '未設定')
1086 print(f" (speaker) {s}: {ns}: (voice) {voice}")
1087 return True
1088
1089
1090 def load_text(self, input_path: str, monologue: bool = False, wait_for_clipboard: bool = True) -> list[tuple[str | None, str]] | None:
1091 """指定された入力元(クリップボードまたはファイル)からテキストを読み込み、
1092 (speaker, text)のリストを生成します。
1093
1094 このメソッドは、グローバル関数 ``load_text`` のラッパーです。
1095
1096 :param input_path: str: テキストの入力元。"clip"またはファイルパス。
1097 :param monologue: bool: Trueの場合、全てを単一の話者のテキストとして扱います。
1098 Falseの場合、行を「話者,テキスト」形式で解析します。
1099 :param wait_for_clipboard: bool: ``input_path`` が "clip" の場合に、
1100 ユーザーがクリップボードにコピーするのを待つか。
1101 :returns: list[tuple[str or None, str]] or None - (話者名, テキスト)のタプルのリスト、
1102 またはエラー発生時はNone。
1103 """
1104 return load_text(input_path, monologue = monologue, wait_for_clipboard = wait_for_clipboard)
1105
1106 def get_speakers_from_dialogue(self, dialogue: list[tuple[str | None, str]]) -> list[str | None]:
1107 """対話データからユニークな話者名のリストを抽出します。
1108
1109 :param dialogue: list[tuple[str or None, str]]: (話者名, テキスト)のタプルのリスト。
1110 :returns: list[str or None] - 対話に含まれるユニークな話者名のリスト。
1111 """
1112 return list(set(s for s, _ in dialogue))
1113
1114 def parse_kv_string(self, kv_string: str | None, keys: list[str] = [], allow_no_key_kv_string: bool = False) -> dict[str | int, str]:
1115 """key=val;key=val 形式の文字列を dict に変換します。
1116
1117 このメソッドは、グローバル関数 ``parse_kv_string`` をラップし、
1118 話者名の正規化を適用します。
1119
1120 :param kv_string: str or None: "key=val;key=val" 形式の文字列。
1121 :param keys: list[str]: キーがない値に割り当てるためのキーのリスト。
1122 :param allow_no_key_kv_string: bool: キーが指定されていないアイテム(例: "value1;value2")を許可するかどうか。
1123 :returns: dict[str or int, str] - 変換された辞書。キーは正規化された話者名またはインデックス。
1124 """
1125
1126 print()
1127 print("parse_kv_string:")
1128 print(" kv_string:", kv_string)
1129 print(" keys:", keys)
1130
1131 if not kv_string: return {}
1132 nkeys = len(keys)
1133
1134 speakers_kv = []
1135 d = {}
1136 idx = 0
1137# kv_stringのspeaker
1138 for item in kv_string.split(";"):
1139 if "=" in item:
1140 k, voice = item.split("=", 1)
1141 speaker = self.normalize_speaker(k.strip())
1142 d[speaker] = voice
1143 if voice not in d.keys(): d[voice] = voice
1144 if speaker not in speakers_kv:
1145 speakers_kv.append(speaker)
1146# print("478 add:", speaker)
1147 elif allow_no_key_kv_string:
1148# = が無い場合は整数idxの辞書をつくる
1149 if "*" in item or item.strip() == "": continue
1150
1151# ユーザ指定辞書keysで与えられたspeaker(読み上げファイルのspeaker)をkv_stringにマップ
1152 if idx < nkeys:
1153 voice = item
1154# speaker = self.normalize_speaker(voice)
1155# voice = self.normalize_speaker(keys[idx])
1156 speaker = self.normalize_speaker(keys[idx])
1157 d[speaker] = voice
1158 if voice not in d.keys(): d[voice] = voice
1159 if speaker not in speakers_kv:
1160 speakers_kv.append(speaker)
1161 d[idx] = item
1162 idx += 1
1163
1164# ユーザ指定辞書keysで与えられたspeaker(読み上げファイルのspeaker)をkv_stringにマップ
1165 for idx, speaker_keys in enumerate(keys):
1166 speaker_keys = self.normalize_speaker(speaker_keys)
1167 if speaker_keys in d.keys(): continue
1168
1169 d[speaker_keys] = speaker_keys
1170
1171 return d
1172
1173 def update_voice_map(self, voice_map: dict = {}, voices: str = "", speakers: list[str | None] = []) -> dict[str | None | int, str]:
1174 """既存の音声マップと新しい音声指定、話者リストに基づいて、
1175 現在のTTSエンジンの音声マッピングを更新します。
1176
1177 ``voices`` 引数が空の場合、``speakers`` リストから話者名を取得し、
1178 それをデフォルトの音声名としてマップします。
1179 ``voices`` が指定されている場合は、それを解析して既存のマップを上書きまたは追加します。
1180
1181 :param voice_map: dict: 全てのTTSエンジンに対する既存の音声マッピング辞書。
1182 :param voices: str: セミコロンで区切られた音声指定文字列
1183 (例: "speaker1=voiceA;speaker2=voiceB;voiceC")。
1184 :param speakers: list[str or None]: 読み上げファイルから検出された話者のリスト。
1185 :returns: dict[str or None or int, str] - 更新された現在のTTSエンジンの音声マッピング辞書。
1186 """
1187 current_voice_map = voice_map.get(self.tts_name, {}).copy()
1188 print(f" Voice maps for [{self.tts_name}]];", current_voice_map)
1189
1190 voices = voices.strip()
1191 if voices == "":
1192 print(f"voices is blank. Use given speakers")
1193 for idx, sp in enumerate(speakers):
1194 sp = self.normalize_speaker(sp)
1195 current_voice_map[sp] = sp
1196 current_voice_map[idx] = sp
1197 else:
1198 print(f"Read voice maps from voices:", voices)
1199# 読み上げファイルから抽出したspeakersは使わない
1200# voices_override = self.parse_kv_string(voices, {}, allow_no_key_kv_string = True)
1201 voices_override = self.parse_kv_string(voices, speakers, allow_no_key_kv_string = True)
1202 current_voice_map.update(voices_override)
1203
1204 current_voice_map[None] = current_voice_map.get(0, None)
1205 current_voice_map[""] = current_voice_map.get(0, None)
1206
1207 return current_voice_map
1208
1209 def speak_dialogue(self, dialogue: list[tuple[str | None, str]] | None = None, voice_map: dict[str | None, str] | str | None = None, speakers: dict | None = None,
1210 replacements: dict[str, str] | None = None, endpoint: str | None = None, api_key: str | None = None,
1211 output_format: str | None = None, config: SimpleNamespace | None = None) -> str | bool:
1212 """現在設定されているTTSエンジンを使用して、対話データから音声を生成し、
1213 結合してファイルに保存または直接再生します。
1214
1215 このメソッドは、グローバル関数 ``speak_dialogue`` を呼び出すラッパーメソッドです。
1216 インスタンスに設定されているエンドポイントやAPIキー、設定オブジェクトを自動的に渡します。
1217
1218 :param dialogue: list[tuple[str or None, str]] or None: (話者名, テキスト)のタプルのリスト。
1219 :param voice_map: dict[str or None, str] or str or None: 話者と音声名をマッピングする辞書、
1220 または単一の音声名文字列。
1221 :param speakers: dict or None: 以前に解析された話者情報。
1222 :param replacements: dict[str, str] or None: テキスト置換ルール。
1223 :param endpoint: str or None: APIを使用するTTSエンジンのエンドポイントURL。
1224 指定しない場合はインスタンスの設定を使用します。
1225 :param api_key: str or None: APIを使用するTTSエンジンのAPIキー。
1226 指定しない場合はインスタンスの設定を使用します。
1227 :param output_format: str or None: 出力音声ファイルの形式(例: "mp3", "wav")。
1228 :param config: SimpleNamespace or None: 設定情報を含むオブジェクト。
1229 指定しない場合はインスタンスの設定を使用します。
1230 :returns: str or bool - 保存されたファイルのパス(str)、
1231 または成功の場合はTrue (直接再生時)、失敗の場合はFalse。
1232 """
1233 if endpoint is None:
1234 endpoint = self.endpoint
1235 if api_key is None:
1236 api_key = self.api_key
1237 if config is None:
1238 config = self.config
1239 return speak_dialogue(
1240 config,
1241 dialogue,
1242 voice_map,
1243 speakers=speakers,
1244 replacements=replacements,
1245 default_voicevox_voice=self.get_default_voice("voicevox"),
1246 default_pyttsx3_voice=self.get_default_voice("pyttsx3"),
1247 default_winrt_voice=self.get_default_voice("winrt"),
1248 default_aqt_preset=self.get_default_voice("atp"), # 辞書キーは "atp" or "aquestalkplayer"
1249 default_optnai_voice=self.get_default_voice("openai"),
1250 default_elevenlabs_voice=self.get_default_voice("elevenlabs"),
1251 default_gemini_voice=self.get_default_voice("gemini"),
1252 endpoint=endpoint,
1253 api_key=api_key,
1254 output_format=output_format,
1255 )