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

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()