"""
tktts.py - 複数のテキスト読み上げ(TTS)エンジンを統合し、音声生成・再生・保存を行うモジュール
概要:
本モジュールは、複数のテキスト読み上げ(TTS)エンジン(pyttsx3, VoiceVox, AquesTalkPlayer, OpenAIなど)を
統一されたインターフェースで利用するための機能を提供します。
テキストファイルの読み込み、話者ごとの音声マッピング、音声ファイルの生成、結合、一時ファイルの管理、
そして最終的な音声の保存または再生までの一連の処理をサポートします。
詳細説明:
- 必要なライブラリのチェックと不足時の案内機能。
- 各TTSエンジンモジュールを安全にインポートする機構。
- テキスト入力元(ファイルまたはクリップボード)からのデータ読み込みと、対話形式/独話形式の解析。
- 話者ごとの音声割り当て(デフォルト音声、指定音声、話者マッピングの更新)。
- 選択されたTTSエンジンによる音声生成(各エンジン固有のパラメータ対応)。
- 生成された複数の音声ファイルの時間間隔を考慮した結合。
- 最終的な音声ファイルの指定フォーマットでの保存またはインメモリ再生(pyttsx3の場合)。
- 一時ディレクトリおよび一時ファイルの自動クリーンアップ。
- `tkTTS`クラスによる、よりオブジェクト指向的なTTS操作の提供。
関連リンク:
:doc:`tktts_usage`
"""
import os
import sys
import re
import time
import subprocess
import shutil
import tempfile
from pathlib import Path
import traceback
import inspect
# --- 必要なライブラリのチェック ---
required_libs = ["pydub", "pyperclip", "chardet", "pyttsx3", "openai", "simpleaudio"]
missing = []
for lib in required_libs:
try:
__import__(lib)
except ImportError:
missing.append(lib)
if missing:
print(f"Error: Missing libraries:\n{', '.join(missing)}")
print("------------------------------------------------------------------")
print("インストールが必要です:")
print(" pip install pyttsx3 openai pydub chardet pyperclip")
print("\n[FFmpeg のインストール]")
print(" pydub, simpleaudio の利用に必要です。公式サイトからダウンロードし、PATHを設定してください。")
print("------------------------------------------------------------------")
input("\nPress ENTER to terminate>>\n")
sys.exit(1)
import pyperclip
import chardet
from pydub import AudioSegment
from pydub.playback import play
import simpleaudio as sa
tktts_pyttsx3 = None
tktts_voicevox = None
tktts_aquestalkplayer = None
tktts_openai = None
def _safe_import(module_name):
"""
モジュールを安全にインポートし、エラーが発生した場合は警告を表示します。
詳細:
モジュールが見つからない場合やインポート中にエラーが発生した場合でもプログラムの実行を中断せず、
エラーメッセージとトレースバックを出力します。
:param module_name: インポートするモジュール名。
:type module_name: str
:returns: インポートされたモジュールオブジェクト、またはインポートに失敗した場合はNone。
:rtype: ModuleType or None
"""
try:
return __import__(module_name)
except Exception as e:
print(f"\nWarning in tktts.py: Import error for {module_name}")
print("------------------------------------------------------------------")
print(f"Error message: {e}")
print("Traceback:")
traceback.print_exc()
print("------------------------------------------------------------------")
return None
tktts_pyttsx3 = _safe_import('tktts_pyttsx3')
tktts_voicevox = _safe_import('tktts_voicevox')
tktts_aquestalkplayer = _safe_import('tktts_aquestalkplayer')
tktts_openai = _safe_import('tktts_openai')
default_pyttsx3_voice = "Zira"
default_voicevox_voice = "四国めたん"
default_aqt_preset = "れいむ"
default_optnai_voice = "alloy"
TTS_ENGINES = {
"pyttsx3": {
"engine": tktts_pyttsx3,
"default_voice": default_pyttsx3_voice,
"ext": "wav"
},
"voicevox": {
"engine": tktts_voicevox,
"default_voice": default_voicevox_voice,
"ext": "wav"
},
"aquestalkplayer": {
"engine": tktts_aquestalkplayer,
"default_voice": default_aqt_preset,
"ext": "wav"
},
"atp": {
"engine": tktts_aquestalkplayer, # aquestalkplayerとエンジンを共有
"default_voice": default_aqt_preset,
"ext": "wav"
},
"openai": {
"engine": tktts_openai,
"default_voice": default_optnai_voice,
"ext": "mp3"
},
}
[ドキュメント]
def apply_replacements(text, replacements):
"""
置換辞書に基づいてテキスト内の指定された文字列を置換します。
詳細:
置換は大文字小文字を無視して行われます。
:param text: 置換を適用する元のテキスト文字列。
:type text: str
:param replacements: {置換前の文字列: 置換後の文字列}形式の辞書。
:type replacements: dict
:returns: 置換が適用されたテキスト文字列。
:rtype: str
"""
for key, val in replacements.items():
text = re.sub(key, val, text, flags=re.IGNORECASE)
return text
[ドキュメント]
def load_text(input_path, monologue = False, wait_for_clipboard = True):
"""
指定された入力元(ファイルまたはクリップボード)からテキストを読み込み、話者とテキストのペアのリストを返します。
詳細:
- `input_path`が"clip"の場合、クリップボードからテキストを取得します。
- ファイルパスの場合、ファイルを読み込み、`chardet`でエンコーディングを自動判別します。
- 読み込まれたテキストを行ごとに処理し、対話形式または独話形式(モノローグ)に応じて`(speaker, text)`のリストを生成します。
- 独話モードでは`speaker`は`None`になります。
- コメント行(`#`で始まる行)や空行はスキップされます。
:param input_path: テキストの入力元。"clip"またはファイルパス。
:type input_path: str
:param monologue: Trueの場合、すべての行を独話として扱い、話者をNoneとします。
:type monologue: bool
:param wait_for_clipboard: input_pathが"clip"の場合、クリップボードへのコピーを待機するかどうか。
:type wait_for_clipboard: bool
:returns: (speaker, text)のタプルのリスト、またはエラーが発生した場合はNone。
:rtype: list[tuple[str or None, str]] or None
"""
print()
print(f"load_text from {input_path}")
if input_path.lower() == "clip" and wait_for_clipboard:
print()
input("読み上げるテキストをクリップボードにコピーしてください:\n")
print("📥 クリップボードからテキストを取得中...")
text = pyperclip.paste()
text_lines = text.splitlines()
else:
file_name = input_path
if not os.path.isfile(file_name):
print(f"Error: ファイル [{file_name}] が見つかりません。")
return None
print(f"📖 ファイル [{file_name}] を読み込み中...")
try:
with open(file_name, "rb") as f:
raw_data = f.read()
result = chardet.detect(raw_data)
encoding = result["encoding"]
if encoding is None: encoding = 'utf-8'
text = raw_data.decode(encoding, errors='ignore')
print(f" 検出されたエンコード: {encoding}")
text_lines = text.splitlines()
except Exception as e:
print(f"❌ ファイル読み込み/エンコード判別エラー: {e}")
return None
# dialogueリストへの変換 (既存ロジックを流用)
dialogue = []
for line in text_lines:
line = line.strip()
if not line: continue
if line.startswith("#"): continue
if monologue:
dialogue.append((None, line.strip()))
else:
if "," in line:
try:
speaker, text = line.split(",", 1)
dialogue.append((speaker.strip(), text.strip()))
except ValueError:
continue
# 対話モードでカンマがない行は無視
# 修正前のコードではカンマがない行は無視されているため、ここでは continue
continue
return dialogue
[ドキュメント]
def create_temp_dir(temp_dir):
"""
指定されたパスに一時ディレクトリを作成します。
詳細:
ディレクトリが存在しない場合にのみ作成されます。
:param temp_dir: 作成する一時ディレクトリのパス。
:type temp_dir: str
:returns: 作成または確認された一時ディレクトリのパス。
:rtype: str
"""
if not os.path.exists(temp_dir):
os.makedirs(temp_dir, exist_ok=True)
print(f"一時ディレクトリを作成: {temp_dir}")
return temp_dir
[ドキュメント]
def get_speaker_dict(tts_engine, dialogue, voices,
default_voicevox_voice = "四国めたん", default_pyttsx3_voice = "Zira",
default_aqt_preset = "れいむ", default_optnai_voice = "alloy"):
"""
対話データと指定された音声設定に基づいて、話者ごとの音声マッピング辞書を生成します。
詳細:
- `voices`引数でセミコロン区切りで指定された音声リストを優先的に使用します。
- 指定がない場合、TTSエンジンごとのデフォルト音声が使用されます。
- 独話(話者なし)の場合やファイル内で定義されていない話者には、デフォルト音声が割り当てられます。
:param tts_engine: 使用するTTSエンジンの名前(例: "pyttsx3", "voicevox")。
:type tts_engine: str
:param dialogue: (speaker, text)のタプルのリスト。
:type dialogue: list[tuple[str or None, str]]
:param voices: セミコロン区切りの音声名リスト(例: "voice1;voice2")。
:type voices: str
:param default_voicevox_voice: VoiceVoxのデフォルト音声名。
:type default_voicevox_voice: str
:param default_pyttsx3_voice: pyttsx3のデフォルト音声名。
:type default_pyttsx3_voice: str
:param default_aqt_preset: AquesTalkPlayerのデフォルトプリセット名。
:type default_aqt_preset: str
:param default_optnai_voice: OpenAIのデフォルト音声名。
:type default_optnai_voice: str
:returns: {話者名: 音声名}形式のマッピング辞書。
:rtype: dict[str or None, str]
"""
if tts_engine in TTS_ENGINES:
default_voice = TTS_ENGINES[tts_engine]["default_voice"]
else:
print(f"\nError in tktts.get_speaker_dict(): Invalid tts engine [{tts_engine}]\n")
return None
print(f"\n話者リスト作成...")
voices_specified = voices.split(';')
if len(voices_specified) > 0:
default_voice = voices_specified[0]
speakers = []
target_voices = {}
ispeaker = 0
for speaker, text in dialogue:
if speaker is not None and speaker != "" and speaker not in speakers:
speakers.append(speaker)
if len(voices_specified) > ispeaker:
target_voices[speaker] = voices_specified[ispeaker]
else:
target_voices[speaker] = default_voice
ispeaker += 1
target_voices[""] = default_voice
target_voices[None] = default_voice
for i, (speaker, voice) in enumerate(target_voices.items()):
print(f" {i:02d}: {speaker} => {voice}")
return target_voices
[ドキュメント]
def get_tts(tts_engine):
"""
指定されたTTSエンジンのモジュールオブジェクトを取得します。
詳細:
`TTS_ENGINES`辞書から対応するエンジンモジュールを検索します。
モジュールがインポートに失敗している場合、エラーメッセージを出力しNoneを返します。
:param tts_engine: 取得したいTTSエンジンの名前。
:type tts_engine: str
:returns: TTSエンジンモジュールオブジェクト、または見つからない/インポート失敗の場合はNone。
:rtype: ModuleType or None
"""
# 📌 修正箇所 2: TTSモジュールオブジェクトの取得
if tts_engine in TTS_ENGINES:
config = TTS_ENGINES[tts_engine]
tts_module = config["engine"]
if tts_module is not None:
return tts_module
else:
# エンジンは登録されているが、インポートに失敗した場合
print(f"\nError in tktts.get_tts(): TTS engine module [tktts_{tts_engine}] failed to import.")
print(f"Scroll back the terminal console to check specific import error message.")
print(f" Run tktts_{tts_engine}.py to check missing libraries in this module")
return None
else:
print(f"\nError in tktts.get_tts(): Invalid tts engine [{tts_engine}]\n")
return None
[ドキュメント]
def get_available_voices_info(endpoint = None):
"""
現在選択されているTTSエンジンが利用可能な音声の詳細情報を取得します。
詳細:
内部で`get_tts`を呼び出し、取得したTTSモジュールの`get_available_voices_info`メソッドを呼び出します。
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。
:type endpoint: str or None
:returns: 利用可能な音声の詳細情報(エンジン依存の形式)、またはエラーの場合はNone。
:rtype: list or dict or None
"""
tts = get_tts(tts_engine)
if tts is None: return None
return tts.get_available_voices_info(endpoint)
[ドキュメント]
def get_available_voices(endpoint = None):
"""
現在選択されているTTSエンジンが利用可能な音声名のリストを取得します。
詳細:
内部で`get_tts`を呼び出し、取得したTTSモジュールの`get_available_voices`メソッドを呼び出します。
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。
:type endpoint: str or None
:returns: 利用可能な音声名のリスト、またはエラーの場合はNone。
:rtype: list[str] or None
"""
tts = get_tts(tts_engine)
if tts is None: return None
return tts.get_available_voices(endpoint)
[ドキュメント]
def list_available_voices(tts_engine, endpoint = None):
"""
指定されたTTSエンジンが利用可能な音声のリストを表示します。
詳細:
`get_tts`を呼び出し、TTSモジュールに`list_available_voices`メソッドがあればそれを呼び出して結果を表示します。
VoiceVoxの場合、`endpoint`引数を使用します。
:param tts_engine: 音声リストを取得するTTSエンジンの名前。
:type tts_engine: str
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。
:type endpoint: str or None
:returns: `list_available_voices`メソッドの実行結果(通常はTrue/False)、またはエラーの場合はFalse。
:rtype: bool
"""
tts = get_tts(tts_engine)
if tts is None: return None
config = TTS_ENGINES.get(tts_engine)
if config is None:
print(f"\nError in tktts.list_available_voices(): Invalid tts engine [{tts_engine}]\n")
return None
ret = False
try:
if hasattr(tts, 'list_available_voices'):
# VoiceVoxなど、endpoint引数を持つ可能性があるものに対応
if tts_engine == "voicevox" and endpoint is not None:
ret = tts.list_available_voices(endpoint=endpoint)
else:
# 引数なしで呼び出す
ret = tts.list_available_voices()
else:
print(f"Error: TTS module [{tts_engine}] has no list_available_voices function.")
except Exception as e:
print(f"Error calling list_available_voices for {tts_engine}: {e}")
ret = False
print("===============================")
return ret
[ドキュメント]
def normalize_speaker(speaker, tts_engine):
"""
話者名を正規化します。
詳細:
話者名からスタイル情報(例: `(元気)`)や括弧を除去し、基本となる話者名のみを抽出します。
現在の実装では、空白で区切られた最初の部分を話者名としています。
:param speaker: 正規化する話者名文字列。
:type speaker: str or None
:param tts_engine: 使用するTTSエンジンの名前(現在は使用されていませんが、将来的な拡張性のため保持)。
:type tts_engine: str
:returns: 正規化された話者名。speakerがNoneの場合はNone。
:rtype: str or None
"""
if speaker is None: return None
# style付きのspeakerに対応
speaker_list = speaker.split('(')
# '(', ')'をreplace.pyで' 'に変換してしまう問題に対応
speaker_list = speaker.split(' ')
if len(speaker_list) > 0: return speaker_list[0]
return speaker
[ドキュメント]
def parse_kv_string(kv_string, keys = [], allow_no_key_kv_string = False):
"""
"key=val;key=val"形式の文字列を辞書に変換します。
詳細:
- セミコロンで区切られた各項目を処理します。
- `=`を含む項目は`key: val`として辞書に追加します。
- `allow_no_key_kv_string`がTrueの場合、`=`を含まない項目は`keys`リストの対応するインデックス、
または連番のインデックスで辞書に追加します。
- `keys`引数で指定された話者が`kv_string`で設定されていない場合、その話者名をそのまま値として追加します。
:param kv_string: "key=val;key=val"形式の入力文字列。
:type kv_string: str
:param keys: `=`を含まない項目に値を割り当てる際に使用するキーのリスト。
:type keys: list[str]
:param allow_no_key_kv_string: `=`を含まない項目を許可するかどうか。
:type allow_no_key_kv_string: bool
:returns: 変換された辞書。
:rtype: dict
"""
if not kv_string: return {}
nkeys = len(keys)
d = {}
idx = 0
for item in kv_string.split(";"):
if "=" in item:
k, v = item.split("=", 1)
d[k.strip()] = v.strip()
elif allow_no_key_kv_string:
if "*" in item or item.strip() == "": continue
if idx < nkeys: d[keys[idx]] = item
d[idx] = item
idx += 1
for speaker in keys:
if speaker in d.keys(): continue
d[speaker] = speaker
return d
[ドキュメント]
def speak_dialogue(args, dialogue, voice_map = None, speakers = {}, replacements = None,
default_voicevox_voice = "四国めたん", default_pyttsx3_voice = "Zira",
default_aqt_preset = "れいむ", default_optnai_voice = "alloy",
endpoint = None, output_format = 'mp3'):
"""
指定されたTTSエンジンを使用して、対話データから音声を生成し、結合して保存または再生します。
詳細:
- `args.tts`で指定されたTTSエンジンを選択します。
- `voice_map`が指定されていない場合、`get_speaker_dict`を呼び出して話者と音声のマッピングを生成します。
- pyttsx3以外のエンジンは一時ファイルへの保存が必須なため、`--outfile`の指定がなければエラーとなります。
- 一時ディレクトリを作成し、各テキストセグメントの音声ファイルを生成します。
- 生成された音声ファイルを`pydub`で結合し、指定された間隔(`args.tinterval`)を挿入します。
- 最終的な音声を`args.outfile`に保存し、一時ファイルを削除します。
:param args: コマンドライン引数を格納するオブジェクト(tts, outfile, monologue, speak_rate, tinterval, temp_dirなどを保持)。
:type args: argparse.Namespace
:param dialogue: (speaker, text)のタプルのリスト。
:type dialogue: list[tuple[str or None, str]]
:param voice_map: {話者名: 音声名}形式のマッピング辞書。Noneの場合、自動生成されます。
:type voice_map: dict[str or None, str] or None
:param speakers: ファイルから検出された話者のリスト。
:type speakers: list[str]
:param replacements: テキスト置換ルールを定義する辞書。
:type replacements: dict or None
:param default_voicevox_voice: VoiceVoxのデフォルト音声名。
:type default_voicevox_voice: str
:param default_pyttsx3_voice: pyttsx3のデフォルト音声名。
:type default_pyttsx3_voice: str
:param default_aqt_preset: AquesTalkPlayerのデフォルトプリセット名。
:type default_aqt_preset: str
:param default_optnai_voice: OpenAIのデフォルト音声名。
:type default_optnai_voice: str
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。
:type endpoint: str or None
:param output_format: 出力オーディオファイルのフォーマット(例: 'mp3', 'wav')。
:type output_format: str
:returns: 成功した場合は出力ファイルのパス、失敗した場合はFalse。
:rtype: str or bool
"""
# print()
print(f"tktts.speak_dialogue(): Generate audio files:")
tts_engine = args.tts.lower()
tts = get_tts(tts_engine)
if tts is None: return False
# エンジン設定を取得
config = TTS_ENGINES.get(tts_engine)
if config is None: return False
outfile = args.outfile
is_save_mode = bool(outfile)
print()
print("speak_dialogue:")
print(f" tts_engine : {tts_engine} is monologue: {args.monologue}"
+ f" speak rate : {args.speak_rate} tinterval : {args.tinterval}")
print(f" voices : {args.voices}")
if is_save_mode:
print(f" outfile : {args.outfile}")
else:
print(f" is_save_mode: {is_save_mode}")
if voice_map is None:
target_voices = get_speaker_dict(tts_engine, dialogue, args.voices,
default_voicevox_voice = default_voicevox_voice,
default_pyttsx3_voice = default_pyttsx3_voice,
default_aqt_preset = default_aqt_preset,
default_optnai_voice = default_optnai_voice)
else:
target_voices = voice_map
# print("target_voices:")
# for key, val in target_voices.items():
# print(f" {key}: {val}")
# pyttsx3以外のエンジンはファイル保存が必須というチェック
# pyttsx3はインメモリ再生が可能なので除外
if tts_engine != "pyttsx3":
if args.outfile is None or args.outfile == "":
print("❌ ファイル保存が必要です。--outfile を指定してください。")
return False
if not args.temp_dir:
print("❌ 一次ファイル保存が必要です。--temp_dir を指定してください。")
return False
temp_dir = create_temp_dir(args.temp_dir)
tmpfiles = []
success = True
# 共通引数
call_kwargs = {
"dialogue": dialogue,
"replacements": replacements,
"target_voices": target_voices,
"speakers": speakers,
"temp_dir": temp_dir,
"outfile": args.outfile,
"ext": config["ext"], # 辞書から取得
"cfg": args
}
# エンジン固有の引数を追加
# inspect.signature()を使って、実際にモジュールがどの引数をサポートしているかチェックするのが理想的だが、
# シンプルに引数名で判断する
if tts_engine == "pyttsx3":
call_kwargs["speak_rate"] = args.speak_rate
elif tts_engine == "voicevox":
if endpoint:
call_kwargs["endpoint"] = endpoint
elif tts_engine == "aquestalkplayer" or tts_engine == "atp":
if hasattr(args, 'aquestalk_path'):
call_kwargs["aquestalk_path"] = args.aquestalk_path
elif tts_engine == "openai":
if hasattr(args, 'instruction'):
call_kwargs["instruction"] = args.instruction
# print()
print(f"\n⚙️ {tts_engine.upper()}で音声ファイルを生成中...")
try:
success, tmpfiles = tts.speak_dialogue(**call_kwargs)
except Exception as e:
print(f"❌ {tts_engine} の speak_dialogue 呼び出しエラー: {e}")
traceback.print_exc()
success = False
if not success: return False
# ファイル結合ロジック (変更なし)
print(" ファイルを結合中...")
combined_audio = AudioSegment.silent(duration=0)
ext = config["ext"] # 辞書から取得したextを使用
# 3. pydubでセグメントを結合
for tmpfile in tmpfiles:
# print(f" add {tmpfile}...")
segment = AudioSegment.from_file(tmpfile, format = ext)
print(f" insert {args.tinterval} sec interval")
tinterval_ms = int(args.tinterval * 1000)
combined_audio += segment + AudioSegment.silent(duration=tinterval_ms)
# 4. クリーンアップと最終出力
# output_format = 'mp3'
if is_save_mode:
combined_audio.export(outfile, format=output_format)
print(f"✅ 出力音声を {outfile} に保存しました (形式: {output_format})。")
time.sleep(0.1) # API/CPU負荷軽減のため
print("🗑️ 一時ファイルを削除中...")
for f in tmpfiles:
try:
os.remove(f)
except:
pass
shutil.rmtree(args.temp_dir)
return outfile
[ドキュメント]
class tkTTS:
"""
複数のテキスト読み上げ(TTS)エンジンを抽象化し、一貫したインターフェースを提供するクラス。
詳細:
内部で各TTSエンジンのモジュールを管理し、話者マッピング、音声リストの取得、
音声生成などの機能を提供します。`set_engine`で利用するTTSエンジンを切り替えることができます。
各種ヘルパー関数(`load_text`, `normalize_speaker`など)をラップしています。
:param tts_name: 初期化時に設定するTTSエンジンの名前。
:type tts_name: str or None
:param config: TTSエンジン設定を含む設定オブジェクト(または辞書)。
:type config: object or dict
"""
def __init__(self, tts_name = None, config = {}):
"""
`tkTTS`クラスのインスタンスを初期化します。
:param tts_name: 初期化時に設定するTTSエンジンの名前。
:type tts_name: str or None
:param config: TTSエンジン設定を含む設定オブジェクト(または辞書)。
:type config: object or dict
:returns: None
:rtype: None
"""
self.tts_name = tts_name
self.tts = None
self.config = config
self.endpoint = config.endpoint if hasattr(config, "endpoint") else None
self.aquestalk_path = config.aquestalk_path if hasattr(config, "aquestalk_path") else None
self.set_engine(tts_name)
[ドキュメント]
def set_engine(self, tts_name):
"""
使用するTTSエンジンを設定します。
:param tts_name: 設定するTTSエンジンの名前。
:type tts_name: str
:returns: None
:rtype: None
"""
self.tts_name = tts_name
self.tts = get_tts(tts_name)
[ドキュメント]
def set_endpoint(self, endpoint):
"""
TTSエンジンのAPIエンドポイントを設定します。
:param endpoint: 設定するAPIエンドポイントURL。
:type endpoint: str
:returns: None
:rtype: None
"""
self.config.endpoint = endpoint
[ドキュメント]
def set_aquestalk_path(self, aquestalk_path):
"""
AquesTalkPlayerの実行可能ファイルのパスを設定します。
:param aquestalk_path: AquesTalkPlayerの実行可能ファイルへのパス。
:type aquestalk_path: str
:returns: None
:rtype: None
"""
self.config.aquestalk_path = aquestalk_path
[ドキュメント]
def get_tts_name(self, tts_name = None):
"""
現在設定されているTTSエンジンの名前、または指定されたTTSエンジンの名前を返します。
:param tts_name: 取得したいTTSエンジンの名前(Noneの場合、インスタンスに設定されている名前を返します)。
:type tts_name: str or None
:returns: TTSエンジンの名前。
:rtype: str
"""
if tts_name is None: return self.tts_name
return tts_name
[ドキュメント]
def get_default_voice(self, tts_name = None):
"""
指定されたTTSエンジンのデフォルト音声名を返します。
:param tts_name: デフォルト音声名を取得するTTSエンジンの名前。Noneの場合、インスタンスに設定されているエンジンのデフォルトを返します。
:type tts_name: str or None
:returns: デフォルト音声名、またはTTSエンジンが見つからない場合はNone。
:rtype: str or None
"""
tts_def = TTS_ENGINES.get(self.get_tts_name(tts_name), None)
if tts_def:return tts_def["default_voice"]
return None
[ドキュメント]
def normalize_speaker(self, speaker, tts_name = None):
"""
話者名を正規化します。
詳細:
クラスのインスタンスメソッドとして、モジュールレベルの`normalize_speaker`関数を呼び出します。
:param speaker: 正規化する話者名文字列。
:type speaker: str or None
:param tts_name: 使用するTTSエンジンの名前(現在は使用されていませんが、将来的な拡張性のため保持)。
:type tts_name: str
:returns: 正規化された話者名。
:rtype: str or None
"""
if tts_name is None: tts_name = self.tts_name
return normalize_speaker(speaker, tts_name)
[ドキュメント]
def list_available_voices(self, tts_name = None, endpoint = None):
"""
指定されたTTSエンジンが利用可能な音声のリストを表示します。
詳細:
クラスのインスタンスメソッドとして、モジュールレベルの`list_available_voices`関数を呼び出します。
:param tts_name: 音声リストを取得するTTSエンジンの名前。Noneの場合、インスタンスに設定されているエンジンの音声リストを表示します。
:type tts_name: str or None
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。Noneの場合、インスタンスに設定されているエンドポイントを使用します。
:type endpoint: str or None
:returns: `list_available_voices`メソッドの実行結果(通常はTrue/False)、またはエラーの場合はFalse。
:rtype: bool
"""
if tts_name is None: tts_name = self.tts_name
if endpoint is None: endpoint = self.endpoint
return list_available_voices(tts_name, endpoint = endpoint)
[ドキュメント]
def show_voice_map(self, infile, voices, VOICE_MAPS, is_monologue, tts_name = None, endpoint = None):
"""
入力ファイルから話者を抽出し、指定された音声マッピングと組み合わせて、最終的な話者-音声マッピングを表示します。
詳細:
- 入力ファイル(`infile`)から話者を検出し、その話者リストを表示します。
- `VOICE_MAPS`と`voices`引数に基づいて`update_voice_map`を呼び出し、
最終的な音声マッピングを生成し表示します。
:param infile: テキスト入力ファイルのパス。
:type infile: str
:param voices: セミコロン区切りの音声指定文字列(例: "speaker1=voiceA;speaker2=voiceB")。
:type voices: str
:param VOICE_MAPS: 既存の音声マッピング辞書。
:type VOICE_MAPS: dict
:param is_monologue: 入力テキストが独話形式かどうか。
:type is_monologue: bool
:param tts_name: 使用するTTSエンジンの名前。
:type tts_name: str or None
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。
:type endpoint: str or None
:returns: テキストデータの取得に成功した場合はTrue、失敗した場合はFalse。
:rtype: bool
"""
print()
print(f"[{infile}]を解析します:")
dialogue = self.load_text(infile, is_monologue, wait_for_clipboard = False)
if not dialogue:
print("エラー: 有効なテキストデータが取得できませんでした。")
if not is_monologue:
print(" 対話形式でない場合は --monologue=1 オプションをつけてください。")
return False
speakers_in_file = self.get_speakers_from_dialogue(dialogue)
print(f" Speakers in [{infile}]")
for idx, sp in enumerate(speakers_in_file):
print(f" {idx:02d}: {sp}")
current_voice_map = self.update_voice_map(voice_map = VOICE_MAPS,
voices = voices, speakers = speakers_in_file)
print()
print(f"Voice map:")
print(f" {'Speaker':<20} => Voice")
for key, val in current_voice_map.items():
if type(key) is str:
print(f" {key:<20} => {val}")
for key, val in current_voice_map.items():
if type(key) is not str and type(key) is not int:
if key is None: key = 'None'
print(f" {key:<20} => {val}")
for key, val in current_voice_map.items():
if type(key) is int:
print(f" {key:<20} => {val}")
print()
print(f"[{infile}] から検出された話者:")
for s in sorted(speakers_in_file):
if s is None or s == "":
voice = current_voice_map.get(s, None)
if voice is None: voice = current_voice_map.get(0, None)
print(f" (独話): {voice}")
else:
ns = self.normalize_speaker(s)
voice = current_voice_map.get(ns, '未設定')
print(f" (speaker) {s}: {ns}: (voice) {voice}")
return True # Added return value for consistency
[ドキュメント]
def load_text(self, input_path, monologue = False, wait_for_clipboard = True):
"""
指定された入力元からテキストを読み込み、話者とテキストのペアのリストを返します。
詳細:
クラスのインスタンスメソッドとして、モジュールレベルの`load_text`関数を呼び出します。
:param input_path: テキストの入力元。"clip"またはファイルパス。
:type input_path: str
:param monologue: Trueの場合、すべての行を独話として扱い、話者をNoneとします。
:type monologue: bool
:param wait_for_clipboard: input_pathが"clip"の場合、クリップボードへのコピーを待機するかどうか。
:type wait_for_clipboard: bool
:returns: (speaker, text)のタプルのリスト、またはエラーが発生した場合はNone。
:rtype: list[tuple[str or None, str]] or None
"""
return load_text(input_path, monologue = monologue, wait_for_clipboard = wait_for_clipboard)
[ドキュメント]
def get_speakers_from_dialogue(self, dialogue):
"""
対話データからユニークな話者のリストを抽出します。
:param dialogue: (speaker, text)のタプルのリスト。
:type dialogue: list[tuple[str or None, str]]
:returns: 対話に含まれるユニークな話者名のリスト。
:rtype: list[str or None]
"""
return list(set(s for s, _ in dialogue))
[ドキュメント]
def parse_kv_string(self, kv_string, keys = [], allow_no_key_kv_string = False):
"""
"key=val;key=val"形式の文字列を辞書に変換します。
詳細:
クラスのインスタンスメソッドとして、モジュールレベルの`parse_kv_string`関数を呼び出し、
話者名の正規化を適用します。
:param kv_string: "key=val;key=val"形式の入力文字列。
:type kv_string: str
:param keys: `=`を含まない項目に値を割り当てる際に使用するキーのリスト(ファイルから検出された話者など)。
:type keys: list[str]
:param allow_no_key_kv_string: `=`を含まない項目を許可するかどうか。
:type allow_no_key_kv_string: bool
:returns: 変換された辞書。
:rtype: dict
"""
print()
print("parse_kv_string:")
print(" kv_string:", kv_string)
print(" keys:", keys)
if not kv_string: return {}
nkeys = len(keys)
speakers_kv = []
d = {}
idx = 0
# kv_stringのspeaker
for item in kv_string.split(";"):
if "=" in item:
k, voice = item.split("=", 1)
speaker = self.normalize_speaker(k.strip())
d[speaker] = voice
if voice not in d.keys(): d[voice] = voice
if speaker not in speakers_kv:
speakers_kv.append(speaker)
# print("478 add:", speaker)
elif allow_no_key_kv_string:
# = が無い場合は整数idxの辞書をつくる
if "*" in item or item.strip() == "": continue
# ユーザ指定辞書keysで与えられたspeaker(読み上げファイルのspeaker)をkv_stringにマップ
if idx < nkeys:
voice = item
# speaker = self.normalize_speaker(voice)
# voice = self.normalize_speaker(keys[idx])
speaker = self.normalize_speaker(keys[idx])
d[speaker] = voice
if voice not in d.keys(): d[voice] = voice
if speaker not in speakers_kv:
speakers_kv.append(speaker)
d[idx] = item
idx += 1
# ユーザ指定辞書keysで与えられたspeaker(読み上げファイルのspeaker)をkv_stringにマップ
for idx, speaker_keys in enumerate(keys):
speaker_keys = self.normalize_speaker(speaker_keys)
if speaker_keys in d.keys(): continue
d[speaker_keys] = speaker_keys
return d
[ドキュメント]
def update_voice_map(self, voice_map = {}, voices = "", speakers = {}):
"""
既存の音声マッピング辞書を更新し、指定された音声設定と話者リストを適用します。
詳細:
- `voice_map`引数で与えられた現在のTTSエンジンのマッピングをコピーします。
- `voices`文字列が空でない場合、`parse_kv_string`を使用して`voices`からマッピングを生成し、
既存のマッピングに上書きします。
- `voices`が空の場合、`speakers`リストから話者名を取得し、話者名をそのまま音声名として
マッピングに追加します。
- 独話(Noneまたは空文字列)の場合のデフォルト音声も設定します。
:param voice_map: 既存の音声マッピング辞書。
:type voice_map: dict
:param voices: セミコロン区切りの音声指定文字列(例: "speaker1=voiceA;speaker2=voiceB")。
:type voices: str
:param speakers: ファイルから検出された話者のリスト。
:type speakers: list[str or None]
:returns: 更新された音声マッピング辞書。
:rtype: dict[str or None or int, str]
"""
current_voice_map = voice_map.get(self.tts_name, {}).copy()
print(f" Voice maps for [{self.tts_name}]];", current_voice_map)
voices = voices.strip()
if voices == "":
print(f"voices is blank. Use given speakers")
for idx, sp in enumerate(speakers):
sp = self.normalize_speaker(sp)
current_voice_map[sp] = sp
current_voice_map[idx] = sp
else:
print(f"Read voice maps from voices:", voices)
# 読み上げファイルから抽出したspeakersは使わない
# voices_override = self.parse_kv_string(voices, {}, allow_no_key_kv_string = True)
voices_override = self.parse_kv_string(voices, speakers, allow_no_key_kv_string = True)
current_voice_map.update(voices_override)
current_voice_map[None] = current_voice_map.get(0, None)
current_voice_map[""] = current_voice_map.get(0, None)
return current_voice_map
[ドキュメント]
def speak_dialogue(self, dialogue = None, voice_map = None, speakers = {}, replacements = None,
endpoint = None, output_format = 'mp3', config = None):
"""
指定されたTTSエンジンと設定を使用して、対話データから音声を生成し、結合して保存または再生します。
詳細:
クラスのインスタンスメソッドとして、モジュールレベルの`speak_dialogue`関数を呼び出します。
インスタンスに設定されている`endpoint`や`config`を自動的に渡します。
:param dialogue: (speaker, text)のタプルのリスト。
:type dialogue: list[tuple[str or None, str]] or None
:param voice_map: {話者名: 音声名}形式のマッピング辞書。Noneの場合、自動生成されます。
:type voice_map: dict[str or None, str] or None
:param speakers: ファイルから検出された話者のリスト。
:type speakers: list[str]
:param replacements: テキスト置換ルールを定義する辞書。
:type replacements: dict or None
:param endpoint: (VoiceVoxなどで使用) TTSエンジンのAPIエンドポイントURL。Noneの場合、インスタンスに設定されているエンドポイントを使用します。
:type endpoint: str or None
:param output_format: 出力オーディオファイルのフォーマット(例: 'mp3', 'wav')。
:type output_format: str
:param config: コマンドライン引数を格納するオブジェクト。Noneの場合、インスタンスに設定されているconfigを使用します。
:type config: object or None
:returns: 成功した場合は出力ファイルのパス、失敗した場合はFalse。
:rtype: str or bool
"""
if endpoint is None: endpoint = self.endpoint
if config is None : config = self.config
return speak_dialogue(config, dialogue, voice_map, speakers = speakers,
replacements = replacements,
default_voicevox_voice = self.get_default_voice("voicevox"),
default_pyttsx3_voice = self.get_default_voice("pyttsx3"),
default_aqt_preset = self.get_default_voice("aqt"),
default_optnai_voice = self.get_default_voice("openai"),
endpoint = endpoint, output_format = output_format)