speak.py ダウンロード/コピー
speak.py
speak.py
1"""統合TTS (pyttsx3, WinRT, VOICEVOX, Qwen3-TTS, Irodori-TTS, AquesTalkPlayer, OpenAI) CLIツール
2
3このモジュールは、複数のテキスト読み上げ (TTS) エンジンを統合し、
4コマンドラインからテキストを音声に変換して再生またはファイルに保存する機能を提供します。
5クリップボードまたは指定されたファイルから入力を受け取り、
6話者認識、ボイスマッピング、文字列置換などの高度な機能もサポートします。
7
8必要なライブラリと外部依存:
9 - chardet: テキストファイルのエンコーディング検出に使用。
10 - pyttsx3: デフォルトのTTSエンジン。
11 - openai: OpenAI TTS APIを使用する場合に必要。
12 - pydub: 音声ファイル操作に使用 (特にAquesTalkPlayerやOpenAIで一時ファイルを結合する際)。
13 - pyperclip: クリップボードからの入力に使用。
14 - tktts: TTSエンジンの統合インターフェースを提供。
15
16外部依存:
17 - ffmpeg.exe: pydubが音声ファイルを処理するために必要です。環境PATHに設定するか、pydubに検出される場所に配置してください。
18 - AquesTalkPlayer.exe: Windowsでのみ使用可能。`--aquestalk_path` オプションで実行パスを指定する必要があります。
19 - OpenAI API Key: OpenAI TTS APIを使用する場合、環境変数 `OPENAI_API_KEY` に設定する必要があります。
20
21関連リンク:
22 :doc:`speak_usage`
23"""
24
25import os
26import sys
27import argparse
28import traceback
29
30missing = []
31for lib in ["chardet", "pyperclip", "tktts"]:
32 try:
33 __import__(lib)
34 except ImportError:
35# missing.append(lib)
36 traceback.print_exc()
37 pass
38
39if missing:
40 print(f"Error: Missing libraries:\n{', '.join(missing)}")
41 print(" install: pip chardet")
42 input("\nPress ENTER to terminate>>\n")
43 sys.exit(1)
44
45import chardet
46import pyperclip
47
48try:
49 import tktts
50 from tktts import tkTTS
51except Exception as e:
52 print(f"\nWarning in tktts.py: Import error for tktts_pyttsx3")
53 print("------------------------------------------------------------------")
54 print(f"Error message: {e}")
55 print("Traceback:")
56 traceback.print_exc()
57 print("------------------------------------------------------------------")
58
59
60# デフォルトの入力元、TTSエンジン、VOICEVOXエンドポイント、AquesTalkPlayerパス、一時ディレクトリ名
61DEFAULT_INPUT = "clip"
62DEFAULT_ENGINE = "pyttsx3"
63DEFAULT_VOICEVOX_ENDPOINT = "http://127.0.0.1:50021"
64DEFAULT_AQUESTALK_PATH = "AquesTalkPlayer.exe"
65DEFAULT_TEMP_DIR = "tts_temp_wavs"
66
67# TTSエンジンごとの話者とボイス名のマッピング
68VOICE_MAPS = {
69 "pyttsx3": {"四国めたん": "Zira", "ずんだもん": "David", "れいむ": "Zira", "まりさ": "David"},
70 "qwen3": {"四国めたん": "Ono_Anna", "ずんだもん": "Ono_Anna", "れいむ": "Ono_Anna", "まりさ": "Ono_Anna"},
71 "qwen": {"四国めたん": "Ono_Anna", "ずんだもん": "Ono_Anna", "れいむ": "Ono_Anna", "まりさ": "Ono_Anna"},
72 "irodori": {"四国めたん": "default", "ずんだもん": "default", "れいむ": "default", "まりさ": "default"},
73 "irodori-tts": {"四国めたん": "default", "ずんだもん": "default", "れいむ": "default", "まりさ": "default"},
74 "aquestalkplayer": {"四国めたん": "れいむ", "ずんだもん": "まりさ", "れいむ": "れいむ", "まりさ": "まりさ", "青山龍星": "青山龍星"},
75 "openai": {"四国めたん": "nova", "ずんだもん": "shimmer", "れいむ": "alloy", "まりさ": "fable"},
76}
77
78# 終了時に入力待ちを行うかどうかのフラグ
79pause = 0
80
81
82def terminate():
83 """プログラムを終了します。
84
85 `pause` グローバル変数が1に設定されている場合、ユーザーがEnterキーを押すまで待機します。
86 """
87 if pause:
88 input("\nPress ENTER to terminate>>\n")
89 exit()
90
91def initialize() -> argparse.Namespace:
92 """コマンドライン引数を解析し、設定オブジェクトを返します。
93
94 この関数は `argparse` モジュールを使用して、TTSエンジン、入力/出力設定、
95 読み上げ速度、パス、および各TTSエンジン固有のオプションなど、
96 様々な設定をコマンドラインから受け取れるように定義します。
97
98 :returns: 解析されたコマンドライン引数を含む `argparse.Namespace` オブジェクト。
99 :rtype: argparse.Namespace
100 """
101 parser = argparse.ArgumentParser(description="統合TTS (pyttsx3, WinRT, VOICEVOX, Qwen3-TTS, Irodori-TTS, AquesTalkPlayer, OpenAI) CLIツール")
102 parser.add_argument("--tts", "-t", choices=["pyttsx3", "winrt", "voicevox", "qwen3", "qwen", "irodori", "irodori-tts", "aquestalkplayer", "atp", "openai"], default=DEFAULT_ENGINE, help="使用するTTSエンジンを選択します。")
103 parser.add_argument("--endpoint", type=str, default=DEFAULT_VOICEVOX_ENDPOINT, help="VOICEVOX EngineのAPIエンドポイントURLを指定します。")
104# parser.add_argument("--language", default = 'japanese', help="pyttsx3 で使用する言語")
105 parser.add_argument("--monologue", "-m", type=int, default=0, help="独話形式(カンマのない行も話者として読み込む)を有効にします。0:無効、1:有効。")
106 parser.add_argument("--voices", "-v", type=str, default="", help="ボイスマッピングを上書きします。(例: key=val;key=val)。話者名と対応するTTSボイス名を指定します。")
107 parser.add_argument("--replace", "-r", type=str, default="", help="テキスト文字列の置換ルールを指定します。(例: key=val;key=val)。")
108
109 parser.add_argument("--infile", "-i", type=str, default=DEFAULT_INPUT, help="入力元の指定。('clip'でクリップボードから、またはファイルパスを指定)。")
110 parser.add_argument("--outfile", "-o", type=str, default="", help="出力する音声ファイルのパスを指定します。(未指定の場合、リアルタイム再生)。")
111
112 parser.add_argument("--temp_dir", type=str, default=DEFAULT_TEMP_DIR, help="一時ファイルを作成するディレクトリ名を指定します。(AquesTalkPlayer/OpenAI使用時)。")
113 parser.add_argument("--list", action="store_true", help="利用可能なボイスの一覧を表示して終了します。")
114 parser.add_argument("--map", action="store_true", help="現在のボイスマッピングを表示して終了します。")
115 parser.add_argument("--pause", "-p", type=int, default=0, help="プログラム終了時に入力待ちをします。(0:無効、1:有効)。")
116 parser.add_argument("--wait_for_clipboard", type=int, default=1, help="クリップボードからテキストを取得する際に入力待ちをします。(0:無効、1:有効)。")
117
118 parser.add_argument("--speak_rate", type=int, default=150, help="pyttsx3 の読み上げ速度をWord Per Minute (WPM) で指定します。")
119 parser.add_argument("--fspeak_rate", type=float, default=1.0, help="VOICEVOX の読み上げ速度比を指定します。(標準: 1.0)。")
120 parser.add_argument("--fspeak_pitch", type=float, default=0.0, help="VOICEVOX の声の高さを指定します。(標準: 0.0)。")
121
122 parser.add_argument("--aquestalk_path", type=str, default=DEFAULT_AQUESTALK_PATH, help="AquesTalkPlayer.exe の実行パスを指定します。(AquesTalkPlayer使用時)。")
123 parser.add_argument("--tinterval", type=float, default=0.5, help="AquesTalkPlayer/OpenAIの音声ファイル間に挿入する無音区間の長さを秒で指定します。(デフォルト 0.5)。")
124
125 # Qwen3-TTS 固有の引数
126 parser.add_argument("--qwen3_language", type=str, default="Japanese", help="Qwen3-TTSで使用する言語を指定します。(デフォルト: Japanese)。")
127 parser.add_argument("--qwen3_model_id", type=str, default="Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice", help="Qwen3-TTSのモデルIDを指定します。")
128 parser.add_argument("--qwen3_device", type=str, default="auto", help="Qwen3-TTSで使用するデバイスを指定します。(例: auto, cuda:0, cpu)。")
129 parser.add_argument("--qwen3_dtype", type=str, default="auto", choices=["auto", "bfloat16", "bf16", "float16", "fp16", "float32", "fp32"], help="Qwen3-TTSのデータ型を指定します。")
130 parser.add_argument("--qwen3_instruct", type=str, default="", help="Qwen3-TTS CustomVoiceへの追加指示を指定します。")
131
132 # Irodori-TTS 固有の引数
133 parser.add_argument("--irodori_caption", type=str, default="落ち着いた自然な声で、明瞭に読み上げる。", help="Irodori-TTSのボイススタイルキャプションを指定します。")
134 parser.add_argument("--irodori_ref_wav", type=str, default="", help="Irodori-TTSのリファレンスWAVファイルパスを指定します。")
135 parser.add_argument("--irodori_ref_wavs", type=str, default="", help="Irodori-TTSのリファレンスWAVファイルパスを複数指定します。(セミコロン区切り)。")
136 parser.add_argument("--irodori_model_id", type=str, default="Aratako/Irodori-TTS-v4.1-Small", help="Irodori-TTSのモデルIDを指定します。")
137 parser.add_argument("--irodori_device", type=str, default="auto", help="Irodori-TTSで使用するデバイスを指定します。(例: auto, cuda, cuda:0, cpu)。")
138 parser.add_argument("--irodori_precision", type=str, default="auto", choices=["auto", "bf16", "bfloat16", "fp32", "float32"], help="Irodori-TTSの精度を指定します。")
139 parser.add_argument("--irodori_codec_device", type=str, default=None, help="Irodoriコーデックのデバイスを指定します。(未指定ならモデルのデバイスを使用)。")
140 parser.add_argument("--irodori_codec_precision", type=str, default=None, help="Irodoriコーデックの精度を指定します。(未指定ならモデルの精度を使用)。")
141 parser.add_argument("--irodori_num_steps", type=int, default=40, help="Irodori-TTSのサンプリングステップ数を指定します。")
142 parser.add_argument("--irodori_cfg_scale_text", type=float, default=3.5, help="Irodori-TTSのテキストに対するCFGスケールを指定します。")
143 parser.add_argument("--irodori_cfg_scale_caption", type=float, default=3.0, help="Irodori-TTSのキャプションに対するCFGスケールを指定します。")
144 parser.add_argument("--irodori_cfg_scale_speaker", type=float, default=5.0, help="Irodori-TTSのスピーカー/リファレンスに対するCFGスケールを指定します。")
145 parser.add_argument("--irodori_duration_scale", type=float, default=1.0, help="Irodori-TTSの長さスケールを指定します。")
146 parser.add_argument("--irodori_seed", type=str, default="0", help="Irodori-TTSのシード値を指定します。('none'または'random'でランダムなシードを使用)。")
147 parser.add_argument("--irodori_lora_adapter", type=str, default="", help="Irodori-TTSのLoRAアダプターのパスを指定します。")
148
149 parser.add_argument("--instruction", type=str, default="", help="OpenAI TTS APIへの追加指示を指定します。(OpenAI使用時)。")
150
151 args = parser.parse_args()
152
153 # 独話でvoice指定が空の場合の既定値
154 if not args.voices:
155 if args.tts in ("qwen3", "qwen"):
156 args.voices = "Ono_Anna"
157 elif args.tts in ("irodori", "irodori-tts"):
158 args.voices = "default"
159
160 # 空文字はバックエンド側では未指定として扱う
161 if args.irodori_ref_wav == "":
162 args.irodori_ref_wav = None
163 if args.irodori_ref_wavs == "":
164 args.irodori_ref_wavs = None
165 if args.irodori_lora_adapter == "":
166 args.irodori_lora_adapter = None
167 if args.qwen3_instruct == "":
168 args.qwen3_instruct = None
169
170 return args
171
172def main():
173 """プログラムのメインエントリポイントです。
174
175 コマンドライン引数を解析し、指定されたTTSエンジンと設定に基づいて、
176 入力テキストを音声に変換し、再生またはファイルに保存する一連の処理を実行します。
177 """
178 global pause
179
180 print()
181 print(f"\n===== 統合TTS CLIツール speak.py =====")
182
183 args = initialize()
184 pause = args.pause
185 if args.infile == "": args.infile = "clip"
186
187 print(f"TTS engine : {args.tts}")
188 print(f"is monologue: {args.monologue}")
189 print(f"Input : {args.infile}")
190 print(f"Output : {args.outfile}")
191# print(f"Language: {args.language}")
192 print(f"pyttsx3 speak_rate: {args.speak_rate}")
193 print(f"VOICEVOX speak_rate: {args.fspeak_rate}")
194 print(f"VOICEVOX speak_pitch: {args.fspeak_pitch}")
195 print(f"VOICEVOX Engine endpoint: {args.endpoint}")
196 print(f"wait_for_clipboard: {args.wait_for_clipboard}")
197
198 if args.tts in ("qwen3", "qwen"):
199 print(f"Qwen3 model : {args.qwen3_model_id}")
200 print(f"Qwen3 device: {args.qwen3_device}")
201 print(f"Qwen3 dtype : {args.qwen3_dtype}")
202 print(f"Qwen3 lang : {args.qwen3_language}")
203 print(f"Qwen3 voice : {args.voices}")
204 print(f"Qwen3 instruct: {args.qwen3_instruct}")
205
206 if args.tts in ("irodori", "irodori-tts"):
207 print(f"Irodori model : {args.irodori_model_id}")
208 print(f"Irodori device : {args.irodori_device}")
209 print(f"Irodori precision: {args.irodori_precision}")
210 print(f"Irodori caption : {args.irodori_caption}")
211 print(f"Irodori ref_wav : {args.irodori_ref_wav}")
212 print(f"Irodori ref_wavs : {args.irodori_ref_wavs}")
213 print(f"Irodori steps : {args.irodori_num_steps}")
214 print(f"Irodori duration : {args.irodori_duration_scale}")
215 print(f"Irodori seed : {args.irodori_seed}")
216
217# endpoint, aquestalk_pathはargsで渡す
218 tktts = tkTTS(tts_name = args.tts, config = args)
219
220 if args.list:
221 print()
222 tktts.list_available_voices()
223 terminate()
224
225 if args.map:
226 print()
227 tktts.show_voice_map(args.infile, args.voices, VOICE_MAPS, args.monologue)
228 terminate()
229
230 print()
231 print(f"[{args.infile}]を解析します:")
232 dialogue = tktts.load_text(args.infile, args.monologue, wait_for_clipboard = args.wait_for_clipboard)
233 if not dialogue:
234 print("エラー: 有効なテキストデータが取得できませんでした。")
235 if not args.monologue:
236 print(" 対話形式でない場合は --monologue=1 オプションをつけてください。")
237 terminate()
238
239 speakers_in_file = tktts.get_speakers_from_dialogue(dialogue)
240 print(f" Speakers in [{args.infile}]")
241 for idx, sp in enumerate(speakers_in_file):
242 print(f" {idx:02d}: {sp}")
243
244 current_voice_map = tktts.update_voice_map(voice_map = VOICE_MAPS,
245 voices = args.voices, speakers = speakers_in_file)
246
247 print()
248 print("=== 置換辞書 ===")
249 replacements = tktts.parse_kv_string(args.replace)
250 if replacements:
251 for key, val in replacements.items():
252 print(f" {key}: {val}")
253 else:
254 print(" (なし)")
255
256 print()
257 print(f"Voice map updated:")
258 for key, val in current_voice_map.items():
259 if type(key) is str:
260 print(f" (speaker) {key}: (voice) {val}")
261 for key, val in current_voice_map.items():
262 if type(key) is not str and type(key) is not int:
263 print(f" (speaker) {key}: (voice) {val}")
264 for key, val in current_voice_map.items():
265 if type(key) is int:
266 print(f" (speaker) {key}: (voice) {val}")
267
268 print("=== 検出された話者とvoice ===")
269 print(f"Voice map updated;", current_voice_map)
270 for s in sorted(speakers_in_file):
271 if s is None or s == "":
272 voice = current_voice_map.get(s, None)
273 if voice is None: voice = current_voice_map.get(0, None)
274 print(f" (独話): {voice}")
275 else:
276 s = tktts.normalize_speaker(s, args.tts)
277 print(f" (speaker) {s}: (voice) {current_voice_map.get(s, '未設定')}")
278
279 print()
280 print("--- 読み上げ処理開始 ---")
281 ret = tktts.speak_dialogue(
282 config = args, dialogue = dialogue,
283 voice_map = current_voice_map, replacements = replacements)
284 if ret is None:
285 terminate()
286
287 print("--- 処理完了 ---")
288
289
290if __name__ == "__main__":
291 main()
292 terminate()