"""統合TTS (pyttsx3, WinRT, VOICEVOX, Qwen3-TTS, Irodori-TTS, AquesTalkPlayer, OpenAI) CLIツール

このモジュールは、複数のテキスト読み上げ (TTS) エンジンを統合し、
コマンドラインからテキストを音声に変換して再生またはファイルに保存する機能を提供します。
クリップボードまたは指定されたファイルから入力を受け取り、
話者認識、ボイスマッピング、文字列置換などの高度な機能もサポートします。

必要なライブラリと外部依存:
    - chardet: テキストファイルのエンコーディング検出に使用。
    - pyttsx3: デフォルトのTTSエンジン。
    - openai: OpenAI TTS APIを使用する場合に必要。
    - pydub: 音声ファイル操作に使用 (特にAquesTalkPlayerやOpenAIで一時ファイルを結合する際)。
    - pyperclip: クリップボードからの入力に使用。
    - tktts: TTSエンジンの統合インターフェースを提供。

外部依存:
    - ffmpeg.exe: pydubが音声ファイルを処理するために必要です。環境PATHに設定するか、pydubに検出される場所に配置してください。
    - AquesTalkPlayer.exe: Windowsでのみ使用可能。`--aquestalk_path` オプションで実行パスを指定する必要があります。
    - OpenAI API Key: OpenAI TTS APIを使用する場合、環境変数 `OPENAI_API_KEY` に設定する必要があります。

関連リンク:
    :doc:`speak_usage`
"""

import os
import sys
import argparse
import traceback

missing = []
for lib in ["chardet", "pyperclip", "tktts"]:
    try:
        __import__(lib)
    except ImportError:
#        missing.append(lib)
        traceback.print_exc()
        pass

if missing:
    print(f"Error: Missing libraries:\n{', '.join(missing)}")
    print("  install: pip  chardet")
    input("\nPress ENTER to terminate>>\n")
    sys.exit(1)

import chardet
import pyperclip

try:
    import tktts
    from tktts import tkTTS
except Exception as e:
    print(f"\nWarning in tktts.py: Import error for tktts_pyttsx3")
    print("------------------------------------------------------------------")
    print(f"Error message: {e}")
    print("Traceback:")
    traceback.print_exc()
    print("------------------------------------------------------------------")


# デフォルトの入力元、TTSエンジン、VOICEVOXエンドポイント、AquesTalkPlayerパス、一時ディレクトリ名
DEFAULT_INPUT = "clip" 
DEFAULT_ENGINE = "pyttsx3"
DEFAULT_VOICEVOX_ENDPOINT = "http://127.0.0.1:50021"
DEFAULT_AQUESTALK_PATH = "AquesTalkPlayer.exe"
DEFAULT_TEMP_DIR = "tts_temp_wavs"

# TTSエンジンごとの話者とボイス名のマッピング
VOICE_MAPS = {
    "pyttsx3": {"四国めたん": "Zira", "ずんだもん": "David", "れいむ": "Zira", "まりさ": "David"},
    "qwen3": {"四国めたん": "Ono_Anna", "ずんだもん": "Ono_Anna", "れいむ": "Ono_Anna", "まりさ": "Ono_Anna"},
    "qwen": {"四国めたん": "Ono_Anna", "ずんだもん": "Ono_Anna", "れいむ": "Ono_Anna", "まりさ": "Ono_Anna"},
    "irodori": {"四国めたん": "default", "ずんだもん": "default", "れいむ": "default", "まりさ": "default"},
    "irodori-tts": {"四国めたん": "default", "ずんだもん": "default", "れいむ": "default", "まりさ": "default"},
    "aquestalkplayer": {"四国めたん": "れいむ", "ずんだもん": "まりさ", "れいむ": "れいむ", "まりさ": "まりさ", "青山龍星": "青山龍星"},
    "openai": {"四国めたん": "nova", "ずんだもん": "shimmer", "れいむ": "alloy", "まりさ": "fable"},
}

# 終了時に入力待ちを行うかどうかのフラグ
pause = 0


def terminate():
    """プログラムを終了します。

    `pause` グローバル変数が1に設定されている場合、ユーザーがEnterキーを押すまで待機します。
    """
    if pause:
        input("\nPress ENTER to terminate>>\n")
    exit()

def initialize() -> argparse.Namespace:
    """コマンドライン引数を解析し、設定オブジェクトを返します。

    この関数は `argparse` モジュールを使用して、TTSエンジン、入力/出力設定、
    読み上げ速度、パス、および各TTSエンジン固有のオプションなど、
    様々な設定をコマンドラインから受け取れるように定義します。

    :returns: 解析されたコマンドライン引数を含む `argparse.Namespace` オブジェクト。
    :rtype: argparse.Namespace
    """
    parser = argparse.ArgumentParser(description="統合TTS (pyttsx3, WinRT, VOICEVOX, Qwen3-TTS, Irodori-TTS, AquesTalkPlayer, OpenAI) CLIツール")
    parser.add_argument("--tts", "-t", choices=["pyttsx3", "winrt", "voicevox", "qwen3", "qwen", "irodori", "irodori-tts", "aquestalkplayer", "atp", "openai"], default=DEFAULT_ENGINE, help="使用するTTSエンジンを選択します。")
    parser.add_argument("--endpoint", type=str, default=DEFAULT_VOICEVOX_ENDPOINT, help="VOICEVOX EngineのAPIエンドポイントURLを指定します。")
#    parser.add_argument("--language", default = 'japanese', help="pyttsx3 で使用する言語")
    parser.add_argument("--monologue", "-m", type=int, default=0, help="独話形式（カンマのない行も話者として読み込む）を有効にします。0:無効、1:有効。")
    parser.add_argument("--voices", "-v", type=str, default="", help="ボイスマッピングを上書きします。（例: key=val;key=val）。話者名と対応するTTSボイス名を指定します。")
    parser.add_argument("--replace", "-r", type=str, default="", help="テキスト文字列の置換ルールを指定します。（例: key=val;key=val）。")

    parser.add_argument("--infile", "-i", type=str, default=DEFAULT_INPUT, help="入力元の指定。（'clip'でクリップボードから、またはファイルパスを指定）。")
    parser.add_argument("--outfile", "-o", type=str, default="", help="出力する音声ファイルのパスを指定します。（未指定の場合、リアルタイム再生）。")
   
    parser.add_argument("--temp_dir", type=str, default=DEFAULT_TEMP_DIR, help="一時ファイルを作成するディレクトリ名を指定します。（AquesTalkPlayer/OpenAI使用時）。")
    parser.add_argument("--list", action="store_true", help="利用可能なボイスの一覧を表示して終了します。")
    parser.add_argument("--map",  action="store_true", help="現在のボイスマッピングを表示して終了します。")
    parser.add_argument("--pause",     "-p", type=int, default=0, help="プログラム終了時に入力待ちをします。（0:無効、1:有効）。")
    parser.add_argument("--wait_for_clipboard", type=int, default=1, help="クリップボードからテキストを取得する際に入力待ちをします。（0:無効、1:有効）。")

    parser.add_argument("--speak_rate", type=int, default=150, help="pyttsx3 の読み上げ速度をWord Per Minute (WPM) で指定します。")
    parser.add_argument("--fspeak_rate", type=float, default=1.0, help="VOICEVOX の読み上げ速度比を指定します。（標準: 1.0）。")
    parser.add_argument("--fspeak_pitch", type=float, default=0.0, help="VOICEVOX の声の高さを指定します。（標準: 0.0）。")

    parser.add_argument("--aquestalk_path", type=str, default=DEFAULT_AQUESTALK_PATH, help="AquesTalkPlayer.exe の実行パスを指定します。（AquesTalkPlayer使用時）。")
    parser.add_argument("--tinterval", type=float, default=0.5, help="AquesTalkPlayer/OpenAIの音声ファイル間に挿入する無音区間の長さを秒で指定します。（デフォルト 0.5）。")

    # Qwen3-TTS 固有の引数
    parser.add_argument("--qwen3_language", type=str, default="Japanese", help="Qwen3-TTSで使用する言語を指定します。（デフォルト: Japanese）。")
    parser.add_argument("--qwen3_model_id", type=str, default="Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice", help="Qwen3-TTSのモデルIDを指定します。")
    parser.add_argument("--qwen3_device", type=str, default="auto", help="Qwen3-TTSで使用するデバイスを指定します。（例: auto, cuda:0, cpu）。")
    parser.add_argument("--qwen3_dtype", type=str, default="auto", choices=["auto", "bfloat16", "bf16", "float16", "fp16", "float32", "fp32"], help="Qwen3-TTSのデータ型を指定します。")
    parser.add_argument("--qwen3_instruct", type=str, default="", help="Qwen3-TTS CustomVoiceへの追加指示を指定します。")

    # Irodori-TTS 固有の引数
    parser.add_argument("--irodori_caption", type=str, default="落ち着いた自然な声で、明瞭に読み上げる。", help="Irodori-TTSのボイススタイルキャプションを指定します。")
    parser.add_argument("--irodori_ref_wav", type=str, default="", help="Irodori-TTSのリファレンスWAVファイルパスを指定します。")
    parser.add_argument("--irodori_ref_wavs", type=str, default="", help="Irodori-TTSのリファレンスWAVファイルパスを複数指定します。（セミコロン区切り）。")
    parser.add_argument("--irodori_model_id", type=str, default="Aratako/Irodori-TTS-v4.1-Small", help="Irodori-TTSのモデルIDを指定します。")
    parser.add_argument("--irodori_device", type=str, default="auto", help="Irodori-TTSで使用するデバイスを指定します。（例: auto, cuda, cuda:0, cpu）。")
    parser.add_argument("--irodori_precision", type=str, default="auto", choices=["auto", "bf16", "bfloat16", "fp32", "float32"], help="Irodori-TTSの精度を指定します。")
    parser.add_argument("--irodori_codec_device", type=str, default=None, help="Irodoriコーデックのデバイスを指定します。（未指定ならモデルのデバイスを使用）。")
    parser.add_argument("--irodori_codec_precision", type=str, default=None, help="Irodoriコーデックの精度を指定します。（未指定ならモデルの精度を使用）。")
    parser.add_argument("--irodori_num_steps", type=int, default=40, help="Irodori-TTSのサンプリングステップ数を指定します。")
    parser.add_argument("--irodori_cfg_scale_text", type=float, default=3.5, help="Irodori-TTSのテキストに対するCFGスケールを指定します。")
    parser.add_argument("--irodori_cfg_scale_caption", type=float, default=3.0, help="Irodori-TTSのキャプションに対するCFGスケールを指定します。")
    parser.add_argument("--irodori_cfg_scale_speaker", type=float, default=5.0, help="Irodori-TTSのスピーカー/リファレンスに対するCFGスケールを指定します。")
    parser.add_argument("--irodori_duration_scale", type=float, default=1.0, help="Irodori-TTSの長さスケールを指定します。")
    parser.add_argument("--irodori_seed", type=str, default="0", help="Irodori-TTSのシード値を指定します。（'none'または'random'でランダムなシードを使用）。")
    parser.add_argument("--irodori_lora_adapter", type=str, default="", help="Irodori-TTSのLoRAアダプターのパスを指定します。")

    parser.add_argument("--instruction", type=str, default="", help="OpenAI TTS APIへの追加指示を指定します。（OpenAI使用時）。")

    args = parser.parse_args()

    # 独話でvoice指定が空の場合の既定値
    if not args.voices:
        if args.tts in ("qwen3", "qwen"):
            args.voices = "Ono_Anna"
        elif args.tts in ("irodori", "irodori-tts"):
            args.voices = "default"

    # 空文字はバックエンド側では未指定として扱う
    if args.irodori_ref_wav == "":
        args.irodori_ref_wav = None
    if args.irodori_ref_wavs == "":
        args.irodori_ref_wavs = None
    if args.irodori_lora_adapter == "":
        args.irodori_lora_adapter = None
    if args.qwen3_instruct == "":
        args.qwen3_instruct = None

    return args

def main():
    """プログラムのメインエントリポイントです。

    コマンドライン引数を解析し、指定されたTTSエンジンと設定に基づいて、
    入力テキストを音声に変換し、再生またはファイルに保存する一連の処理を実行します。
    """
    global pause

    print()
    print(f"\n===== 統合TTS CLIツール speak.py =====")

    args = initialize()
    pause = args.pause
    if args.infile == "": args.infile = "clip"

    print(f"TTS engine  : {args.tts}")
    print(f"is monologue: {args.monologue}")
    print(f"Input       : {args.infile}")
    print(f"Output      : {args.outfile}")
#    print(f"Language: {args.language}")
    print(f"pyttsx3  speak_rate: {args.speak_rate}")
    print(f"VOICEVOX speak_rate: {args.fspeak_rate}")
    print(f"VOICEVOX speak_pitch: {args.fspeak_pitch}")
    print(f"VOICEVOX Engine endpoint: {args.endpoint}")
    print(f"wait_for_clipboard: {args.wait_for_clipboard}")

    if args.tts in ("qwen3", "qwen"):
        print(f"Qwen3 model : {args.qwen3_model_id}")
        print(f"Qwen3 device: {args.qwen3_device}")
        print(f"Qwen3 dtype : {args.qwen3_dtype}")
        print(f"Qwen3 lang  : {args.qwen3_language}")
        print(f"Qwen3 voice : {args.voices}")
        print(f"Qwen3 instruct: {args.qwen3_instruct}")

    if args.tts in ("irodori", "irodori-tts"):
        print(f"Irodori model    : {args.irodori_model_id}")
        print(f"Irodori device   : {args.irodori_device}")
        print(f"Irodori precision: {args.irodori_precision}")
        print(f"Irodori caption  : {args.irodori_caption}")
        print(f"Irodori ref_wav  : {args.irodori_ref_wav}")
        print(f"Irodori ref_wavs : {args.irodori_ref_wavs}")
        print(f"Irodori steps    : {args.irodori_num_steps}")
        print(f"Irodori duration : {args.irodori_duration_scale}")
        print(f"Irodori seed     : {args.irodori_seed}")

# endpoint, aquestalk_pathはargsで渡す
    tktts = tkTTS(tts_name = args.tts, config = args)

    if args.list:
        print()
        tktts.list_available_voices()
        terminate()

    if args.map:
        print()
        tktts.show_voice_map(args.infile, args.voices, VOICE_MAPS, args.monologue)
        terminate()

    print()
    print(f"[{args.infile}]を解析します:")
    dialogue = tktts.load_text(args.infile, args.monologue, wait_for_clipboard = args.wait_for_clipboard)
    if not dialogue:
        print("エラー: 有効なテキストデータが取得できませんでした。")
        if not args.monologue:
            print("  対話形式でない場合は --monologue=1 オプションをつけてください。")
        terminate()

    speakers_in_file = tktts.get_speakers_from_dialogue(dialogue)
    print(f"  Speakers in [{args.infile}]")
    for idx, sp in enumerate(speakers_in_file):
        print(f"    {idx:02d}: {sp}")

    current_voice_map = tktts.update_voice_map(voice_map = VOICE_MAPS, 
                            voices = args.voices, speakers = speakers_in_file)

    print()
    print("=== 置換辞書 ===")
    replacements = tktts.parse_kv_string(args.replace)
    if replacements:
        for key, val in replacements.items():
            print(f"  {key}: {val}")
    else:
        print("  (なし)")

    print()
    print(f"Voice map updated:")
    for key, val in current_voice_map.items():
        if type(key) is str:
            print(f"  (speaker) {key}: (voice) {val}")
    for key, val in current_voice_map.items():
        if type(key) is not str and type(key) is not int:
            print(f"  (speaker) {key}: (voice) {val}")
    for key, val in current_voice_map.items():
        if type(key) is int:
            print(f"  (speaker) {key}: (voice) {val}")
                
    print("=== 検出された話者とvoice ===")
    print(f"Voice map updated;", current_voice_map)
    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:
            s = tktts.normalize_speaker(s, args.tts)
            print(f"  (speaker) {s}: (voice) {current_voice_map.get(s, '未設定')}")

    print()
    print("--- 読み上げ処理開始 ---")
    ret = tktts.speak_dialogue(
        config = args, dialogue = dialogue, 
        voice_map = current_voice_map, replacements = replacements)
    if ret is None:
        terminate()

    print("--- 処理完了 ---")


if __name__ == "__main__":
    main()
    terminate()