"""tktts_* ライブラリファミリーのためのIrodori-TTSバックエンド。

このモジュールは ``tktts_voicevox.py`` や ``tktts_qwen3.py`` で使用されるインターフェースに準拠しています。
Irodori-TTSの公式 ``InferenceRuntime`` API を介して直接呼び出しを行います。
Gradio UI や HTTPサーバーを実行する必要はありません。
ロードされたランタイムはIrodori-TTSによってキャッシュされ、以降のすべての発話で再利用されます。

テキストの分割、読み上げ/発音変換は意図的に呼び出し元に任されています。
``speak_dialogue`` は、他のバックエンドとまったく同様に、tkttsによって渡された置換辞書のみを適用します。

.. seealso::
   :doc:`tktts_irodori_usage`

"""

from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Any, Iterable


missing = []
for lib, import_name in [
    ("torch", "torch"),
    ("Irodori-TTS", "irodori_tts"),
]:
    try:
        __import__(import_name)
    except ImportError:
        missing.append(lib)

if missing:
    raise ImportError(
        "Error: Missing libraries: "
        + ", ".join(missing)
        + "\n  Run this module in the Irodori-TTS uv environment."
        + "\n  install: uv sync --extra cu128"
    )

import torch
from irodori_tts.inference_runtime import (
    RuntimeKey,
    SamplingRequest,
    clear_cached_runtime,
    default_runtime_device,
    download_hf_checkpoint,
    get_cached_runtime,
    save_wav,
)

try:
    from tktts_base import apply_replacements, normalize_speaker, split_dialogue
except ImportError as exc:
    raise ImportError(
        "tktts_irodori requires tktts_base.py to be importable."
    ) from exc


TTS_ENGINE_NAME = "irodori"
DEFAULT_IRODORI_MODEL = "Aratako/Irodori-TTS-v4.1-Small"
DEFAULT_IRODORI_VOICE = "default"
DEFAULT_CAPTION = "落ち着いた自然な声で、明瞭に読み上げる。"
DEFAULT_DEVICE = "auto"
DEFAULT_PRECISION = "auto"
DEFAULT_NUM_STEPS = 40
DEFAULT_CFG_SCALE_TEXT = 3.5
DEFAULT_CFG_SCALE_CAPTION = 3.0
DEFAULT_CFG_SCALE_SPEAKER = 5.0
DEFAULT_SEED = 0


_AVAILABLE_VOICES = [
    {
        "name": DEFAULT_IRODORI_VOICE,
        "description": (
            "Irodori-TTS voice. Use irodori_caption and/or reference WAV "
            "files to define voice identity and speaking style."
        ),
    }
]


def _resolve_device(device: str | None = DEFAULT_DEVICE) -> str:
    """Irodori-TTSが推奨する利用可能なデバイスに ``auto`` を解決します。

    :param device: デバイス名 (例: "cpu", "cuda", "cuda:0", "auto")。デフォルトは "auto"。
    :type device: str | None
    :returns: 解決されたデバイス名。 "cuda:0" は "cuda" に正規化されます。
    :rtype: str
    """
    if device is None or str(device).strip().lower() == "auto":
        return default_runtime_device()
    value = str(device).strip().lower()
    if value == "cuda:0":
        return "cuda"
    return value


def _resolve_precision(device: str, precision: str | None) -> str:
    """``auto`` をCUDA/XPU上のbf16、それ以外ではfp32に解決します。

    :param device: 解決されたデバイス名 (例: "cpu", "cuda")。
    :type device: str
    :param precision: 精度名 (例: "fp32", "bf16", "auto")。
    :type precision: str | None
    :returns: 解決された精度名。
    :rtype: str
    :raises ValueError: サポートされていない精度が指定された場合、またはbf16が対応していないデバイスで指定された場合。
    """
    value = "auto" if precision is None else str(precision).strip().lower()
    if value == "auto":
        return "bf16" if device in {"cuda", "xpu"} else "fp32"
    aliases = {
        "bfloat16": "bf16",
        "bf16": "bf16",
        "float32": "fp32",
        "fp32": "fp32",
    }
    if value not in aliases:
        raise ValueError(f"Unsupported Irodori-TTS precision: {precision}")
    resolved = aliases[value]
    if resolved == "bf16" and device not in {"cuda", "xpu"}:
        raise ValueError(f"bf16 is not supported for Irodori-TTS device={device}")
    return resolved


def _resolve_checkpoint(model_id: str) -> str:
    """ローカルチェックポイントを解決するか、Hugging Faceチェックポイントをダウンロードします。

    指定された ``model_id`` がファイルパスであればその絶対パスを返し、
    そうでなければHugging Faceからダウンロードしてそのパスを返します。

    :param model_id: モデルID (Hugging Faceのリポジトリ名またはローカルファイルパス)。
    :type model_id: str
    :returns: 解決されたチェックポイントファイルの絶対パス。
    :rtype: str
    :raises FileNotFoundError: ローカルファイルとして指定されたチェックポイントが見つからない場合。
    """
    candidate = Path(str(model_id)).expanduser()
    if candidate.suffix.lower() in {".pt", ".safetensors"}:
        if not candidate.is_file():
            raise FileNotFoundError(f"Irodori-TTS checkpoint not found: {candidate}")
        return str(candidate.resolve())
    return str(download_hf_checkpoint(str(model_id)))


def load_model(
    model_id: str = DEFAULT_IRODORI_MODEL,
    device: str = DEFAULT_DEVICE,
    precision: str = DEFAULT_PRECISION,
    *,
    codec_device: str | None = None,
    codec_precision: str | None = None,
    force_reload: bool = False,
):
    """Irodori-TTS推論ランタイムをロードし、キャッシュします。

    この関数は、指定されたモデル、デバイス、精度に基づいてIrodori-TTSのランタイムを初期化します。
    ランタイムはIrodori-TTSの内部キャッシュメカニズムによって管理され、
    同じ設定が再度要求された場合は既存のインスタンスが再利用されます。

    :param model_id: ロードするIrodori-TTSモデルのHugging FaceリポジトリIDまたはローカルパス。デフォルトは ``DEFAULT_IRODORI_MODEL``。
    :type model_id: str
    :param device: モデルの推論に使用するデバイス ("cpu", "cuda", "auto" など)。デフォルトは ``DEFAULT_DEVICE``。
    :type device: str
    :param precision: モデルの推論に使用する精度 ("fp32", "bf16", "auto" など)。デフォルトは ``DEFAULT_PRECISION``。
    :type precision: str
    :param codec_device: コーデックの推論に使用するデバイス。指定されない場合は ``device`` が使用されます。
    :type codec_device: str | None
    :param codec_precision: コーデックの推論に使用する精度。指定されない場合は ``precision`` が使用されます。
    :type codec_precision: str | None
    :param force_reload: Trueの場合、既存のキャッシュされたランタイムをクリアして強制的に再ロードします。
    :type force_reload: bool
    :returns: ロードされたIrodori-TTSの ``InferenceRuntime`` インスタンス。
    :rtype: irodori_tts.inference_runtime.InferenceRuntime
    """
    resolved_device = _resolve_device(device)
    resolved_codec_device = _resolve_device(codec_device or resolved_device)
    resolved_precision = _resolve_precision(resolved_device, precision)
    resolved_codec_precision = _resolve_precision(
        resolved_codec_device,
        precision if codec_precision is None else codec_precision,
    )
    checkpoint = _resolve_checkpoint(model_id)

    if force_reload:
        clear_cached_runtime()

    key = RuntimeKey(
        checkpoint=checkpoint,
        model_device=resolved_device,
        codec_repo="Aratako/Semantic-DACVAE-Japanese-32dim",
        model_precision=resolved_precision,
        codec_device=resolved_codec_device,
        codec_precision=resolved_codec_precision,
        compile_model=False,
        compile_dynamic=False,
    )
    runtime, reloaded = get_cached_runtime(key)
    status = "loaded" if reloaded else "reused"
    print(
        "tktts_irodori.load_model(): "
        f"{status}, model={model_id}, device={resolved_device}, "
        f"precision={resolved_precision}"
    )
    return runtime


def unload_models() -> None:
    """Irodori-TTSによってキャッシュされたランタイムを解放します。

    これにより、Irodori-TTSの推論ランタイムがキャッシュからクリアされ、
    GPUメモリを使用している場合はGPUキャッシュも空にされます。
    """
    clear_cached_runtime()
    if torch.cuda.is_available():
        torch.cuda.empty_cache()


def get_available_voices_info(model=None):
    """Irodori-TTSが使用する仮想的なデフォルト音声の情報を返します。

    Irodori-TTSは固定された話者リストを持っていません。
    声の同一性は参照オーディオによって制御され、スタイルはキャプションによっても制御できます。
    ``model`` 引数はtkttsインターフェースの互換性のために受け入れられますが、内部では使用されません。

    :param model: tkttsインターフェース互換性のため。実際には使用されません。
    :type model: Any
    :returns: 仮想的なデフォルト音声の詳細情報を含む辞書のリスト。
    :rtype: list[dict[str, str]]
    """
    del model
    return [voice.copy() for voice in _AVAILABLE_VOICES]


def get_available_voices(model=None):
    """tkttsの音声セレクタと互換性のある音声名を返します。

    :param model: tkttsインターフェース互換性のため。実際には使用されません。
    :type model: Any
    :returns: 利用可能な仮想音声名のリスト。
    :rtype: list[str]
    """
    return [voice["name"] for voice in get_available_voices_info(model)]


def list_available_voices(model=None) -> bool:
    """Irodori-TTSの仮想的な音声エントリを表示します。

    :param model: tkttsインターフェース互換性のため。実際には使用されません。
    :type model: Any
    :returns: 利用可能な音声がある場合はTrue、ない場合はFalse。
    :rtype: bool
    """
    print(f"=== 利用可能な {TTS_ENGINE_NAME} voices ===")
    voices = get_available_voices_info(model)
    for voice in voices:
        print(f"  Name: {voice['name']}, Description: {voice['description']}")
    return bool(voices)


def _speaker_key(name: str) -> str:
    """話者名を正規化し、比較用のキーを生成します。

    スペース、アンダースコア、ハイフン、括弧を除去し、小文字に変換します。

    :param name: 話者名。
    :type name: str
    :returns: 正規化された話者キー。
    :rtype: str
    """
    normalized = normalize_speaker(str(name))
    return re.sub(r"[\s_\-（）()]+", "", normalized).lower()


def resolve_speaker_id(speaker_name, voices_info=None, model=None):
    """``default``/``irodori`` を仮想的なIrodori音声に解決します。

    Irodori-TTSは固定の話者IDを持たないため、指定された話者名が
    ``default`` またはそのエイリアスであれば、仮想的なIrodori-TTS音声として扱われます。

    :param speaker_name: 解決する話者名。
    :type speaker_name: str
    :param voices_info: 利用可能な音声情報のリスト。Noneの場合は ``get_available_voices_info()`` から取得されます。
    :type voices_info: list[dict[str, str]] | None
    :param model: tkttsインターフェース互換性のため。実際には使用されません。
    :type model: Any
    :returns: (voices_info, 解決された話者名) のタプル。
    :rtype: tuple[list[dict[str, str]], str]
    :raises ValueError: 話者名が見つからない場合。
    """
    del model
    if voices_info is None:
        voices_info = get_available_voices_info()

    query = _speaker_key(speaker_name)
    aliases = {
        "default",
        "irodori",
        "irodoritts",
        "標準",
        "デフォルト",
    }
    if query in aliases:
        return voices_info, DEFAULT_IRODORI_VOICE

    for voice in voices_info:
        if query == _speaker_key(voice["name"]):
            return voices_info, voice["name"]

    raise ValueError(
        "❌ Error in tktts_irodori.resolve_speaker_id(): "
        f"話者 [{speaker_name}] が見つかりませんでした。"
        " Irodori-TTSでは voice=default を使用し、"
        "irodori_captionまたは参照WAVで声を指定してください。"
    )


def _as_optional_int(value: Any, default: int | None) -> int | None:
    """値を整数に変換し、Noneや特定の文字列をNoneとして扱います。

    :param value: 変換する値。
    :type value: Any
    :param default: 値がNoneまたは特定の文字列の場合に返すデフォルト値。
    :type default: int | None
    :returns: 整数またはNone。
    :rtype: int | None
    """
    if value is None:
        return default
    if isinstance(value, str):
        text = value.strip().lower()
        if text in {"", "none", "random"}:
            return None
    return int(value)


def _as_ref_wavs(value: Any) -> list[str]:
    """パス、イテラブル、またはセミコロン区切りのパスを正規化します。

    :param value: 参照WAVファイルのパス、またはパスのイテラブル、またはセミコロン区切りのパス文字列。
    :type value: Any
    :returns: 解決された参照WAVファイルの絶対パスのリスト。
    :rtype: list[str]
    :raises FileNotFoundError: 指定された参照WAVファイルが見つからない場合。
    """
    if value is None:
        return []
    if isinstance(value, (str, os.PathLike)):
        text = str(value).strip()
        if not text:
            return []
        values = [part.strip() for part in text.split(";") if part.strip()]
    elif isinstance(value, Iterable):
        values = [str(part).strip() for part in value if str(part).strip()]
    else:
        values = [str(value).strip()]

    paths = []
    for item in values:
        path = Path(item).expanduser()
        if not path.is_file():
            raise FileNotFoundError(f"Irodori-TTS reference WAV not found: {path}")
        paths.append(str(path.resolve()))
    return paths


def speak(
    outfile,
    text,
    voice=DEFAULT_IRODORI_VOICE,
    speak_rate=None,
    speak_pitch=None,
    *,
    caption: str | None = DEFAULT_CAPTION,
    ref_wav: str | os.PathLike | None = None,
    ref_wavs: Iterable[str | os.PathLike] | str | None = None,
    model=None,
    model_id: str = DEFAULT_IRODORI_MODEL,
    device: str = DEFAULT_DEVICE,
    precision: str = DEFAULT_PRECISION,
    codec_device: str | None = None,
    codec_precision: str | None = None,
    num_steps: int = DEFAULT_NUM_STEPS,
    cfg_scale_text: float = DEFAULT_CFG_SCALE_TEXT,
    cfg_scale_caption: float = DEFAULT_CFG_SCALE_CAPTION,
    cfg_scale_speaker: float = DEFAULT_CFG_SCALE_SPEAKER,
    duration_scale: float = 1.0,
    seed: int | str | None = DEFAULT_SEED,
    lora_adapter: str | None = None,
):
    """Irodori-TTS v4.1-Smallを使用して1つのWAVファイルを生成します。

    テキストを音声に変換し、指定された出力ファイルに保存します。
    Irodori-TTSは固定話者を持たないため、``voice`` 引数は主に情報として扱われます。
    音声のスタイルや同一性は、``caption`` および/または ``ref_wav``/``ref_wavs`` を使用して制御されます。

    :param outfile: 生成されたWAVファイルを保存するパス。
    :type outfile: str | os.PathLike
    :param text: 読み上げるテキスト。
    :type text: str
    :param voice: 読み上げに使用する音声名。Irodori-TTSでは仮想的な音声であり、 ``default`` が推奨されます。デフォルトは ``DEFAULT_IRODORI_VOICE``。
    :type voice: str
    :param speak_rate: 音声の速さ。Irodori-TTSでは直接制御できないため無視されます。
    :type speak_rate: float | None
    :param speak_pitch: 音声のピッチ。Irodori-TTSでは直接制御できないため無視されます。
    :type speak_pitch: float | None
    :param caption: 音声のスタイルを制御するためのキャプション。例: "落ち着いた自然な声で、明瞭に読み上げる。"。デフォルトは ``DEFAULT_CAPTION``。
    :type caption: str | None
    :param ref_wav: スタイル参照に使用する単一のWAVファイルのパス。 ``ref_wavs`` と同時に指定された場合、結合されます。
    :type ref_wav: str | os.PathLike | None
    :param ref_wavs: スタイル参照に使用する複数のWAVファイルのパスのイテラブルまたはセミコロン区切りの文字列。
    :type ref_wavs: Iterable[str | os.PathLike] | str | None
    :param model: ロード済みのIrodori-TTSランタイムインスタンス。指定されない場合、 ``load_model`` を使って新規にロードされます。
    :type model: irodori_tts.inference_runtime.InferenceRuntime | None
    :param model_id: ロードするIrodori-TTSモデルID (``model`` がNoneの場合にのみ使用されます)。デフォルトは ``DEFAULT_IRODORI_MODEL``。
    :type model_id: str
    :param device: モデルの推論に使用するデバイス (``model`` がNoneの場合にのみ使用されます)。デフォルトは ``DEFAULT_DEVICE``。
    :type device: str
    :param precision: モデルの推論に使用する精度 (``model`` がNoneの場合にのみ使用されます)。デフォルトは ``DEFAULT_PRECISION``。
    :type precision: str
    :param codec_device: コーデックの推論に使用するデバイス (``model`` がNoneの場合にのみ使用されます)。
    :type codec_device: str | None
    :param codec_precision: コーデックの推論に使用する精度 (``model`` がNoneの場合にのみ使用されます)。
    :type codec_precision: str | None
    :param num_steps: サンプリングステップ数。デフォルトは ``DEFAULT_NUM_STEPS``。
    :type num_steps: int
    :param cfg_scale_text: テキストに対するCFGスケール。デフォルトは ``DEFAULT_CFG_SCALE_TEXT``。
    :type cfg_scale_text: float
    :param cfg_scale_caption: キャプションに対するCFGスケール。デフォルトは ``DEFAULT_CFG_SCALE_CAPTION``。
    :type cfg_scale_caption: float
    :param cfg_scale_speaker: 話者に対するCFGスケール (参照WAVがある場合のみ有効)。デフォルトは ``DEFAULT_CFG_SCALE_SPEAKER``。
    :type cfg_scale_speaker: float
    :param duration_scale: 音声の全体的な長さを調整するスケールファクター。デフォルトは 1.0。
    :type duration_scale: float
    :param seed: 乱数シード。None, "random" または空文字列の場合、シードはランダムに生成されます。デフォルトは ``DEFAULT_SEED``。
    :type seed: int | str | None
    :param lora_adapter: 使用するLoRAアダプターのパス。
    :type lora_adapter: str | None
    :returns: 生成されたWAVファイルの絶対パス。生成に失敗した場合はNone。
    :rtype: str | None
    """
    text = str(text).strip()
    if not text:
        print("❌ tktts_irodori.speak(): 読み上げテキストが空です")
        return None

    if _speaker_key(voice) not in {
        "default",
        "irodori",
        "irodoritts",
        "標準",
        "デフォルト",
    }:
        print(
            f"    ** Warning: Irodori-TTSには固定話者 [{voice}] がないため "
            "voice=defaultとして扱います"
        )
    if speak_rate is not None or speak_pitch is not None:
        print(
            "    ** Warning: Irodori-TTSでは speak_rate/speak_pitch を"
            "直接指定できないため無視します"
        )

    references = _as_ref_wavs(ref_wavs)
    if ref_wav is not None:
        references = _as_ref_wavs(ref_wav) + references

    if model is None:
        model = load_model(
            model_id=model_id,
            device=device,
            precision=precision,
            codec_device=codec_device,
            codec_precision=codec_precision,
        )

    try:
        result = model.synthesize(
            SamplingRequest(
                text=text,
                caption=str(caption).strip() if caption else None,
                ref_wav=None,
                ref_wavs=references or None,
                no_ref=not references,
                ref_normalize_db=-16.0,
                ref_ensure_max=True,
                num_candidates=1,
                decode_mode="sequential",
                seconds=None,
                duration_scale=float(duration_scale),
                max_ref_seconds=None,
                num_steps=int(num_steps),
                seed=_as_optional_int(seed, DEFAULT_SEED),
                cfg_guidance_mode="independent",
                cfg_scale_text=float(cfg_scale_text),
                cfg_scale_caption=float(cfg_scale_caption),
                cfg_scale_speaker=(float(cfg_scale_speaker) if references else 0.0),
                cfg_min_t=0.5,
                cfg_max_t=1.0,
                context_kv_cache=True,
                t_schedule_mode="linear",
                sway_coeff=-1.0,
                trim_tail=True,
                lora_adapter=lora_adapter,
            ),
            log_fn=lambda message: print(f"    {message}"),
        )
        output_path = save_wav(outfile, result.audio, result.sample_rate)
    except Exception as exc:
        print(f"❌ Irodori-TTS生成エラー: {exc}")
        return None

    if Path(output_path).exists():
        print(
            f"    ** 一時ファイル [{output_path}] を保存しました "
            f"(seed={result.used_seed})"
        )
        return str(output_path)

    print(f"    ** Error: ファイル [{output_path}] の出力に失敗しました")
    return None


def _cfg_value(cfg, name: str, default: Any = None) -> Any:
    """設定オブジェクトから属性値を取得します。

    :param cfg: 設定オブジェクト。Noneの場合、デフォルト値を返します。
    :type cfg: Any
    :param name: 取得する属性の名前。
    :type name: str
    :param default: 属性が見つからない場合、または ``cfg`` がNoneの場合に返すデフォルト値。
    :type default: Any
    :returns: 設定値またはデフォルト値。
    :rtype: Any
    """
    if cfg is None:
        return default
    return getattr(cfg, name, default)


def speak_dialogue(
    dialogue,
    replacements,
    target_voices,
    speakers=None,
    temp_dir=".",
    outfile=None,
    ext="wav",
    cfg=None,
    *,
    caption: str | None = None,
    ref_wav: str | os.PathLike | None = None,
    ref_wavs: Iterable[str | os.PathLike] | str | None = None,
    model=None,
    model_id: str = DEFAULT_IRODORI_MODEL,
    device: str = DEFAULT_DEVICE,
    precision: str = DEFAULT_PRECISION,
):
    """tkttsダイアログシーケンスのための一時WAVファイルを生成します。

    与えられたダイアログ（会話のリスト）を処理し、各発話に対してIrodori-TTSを使用して
    一時的なWAVファイルを生成します。
    ``cfg`` オブジェクトを介してIrodori-TTS固有のパラメータ（例: ``irodori_caption``, ``irodori_seed``）を
    細かく制御できます。

    :param dialogue: 処理するダイアログアイテムのリスト。各アイテムは ``split_dialogue`` が処理できる形式です。
    :type dialogue: list
    :param replacements: テキストに適用する置換辞書。
    :type replacements: dict[str, str]
    :param target_voices: ターゲットとする音声名のリストまたは単一の音声名。
    :type target_voices: list[str] | str
    :param speakers: 話者IDと音声名のマッピング辞書。デフォルトは空の辞書。
    :type speakers: dict[str, str] | None
    :param temp_dir: 一時的なWAVファイルを保存するディレクトリ。デフォルトはカレントディレクトリ。
    :type temp_dir: str | os.PathLike
    :param outfile: tkttsインターフェース互換性のため。実際には使用されません。
    :type outfile: Any
    :param ext: 生成される音声ファイルの拡張子。デフォルトは "wav"。
    :type ext: str
    :param cfg: 設定オブジェクト。このオブジェクトの属性（例: ``cfg.irodori_caption``）を通じて、
                Irodori-TTSの各種パラメータをオーバーライドできます。
    :type cfg: Any
    :param caption: 音声のスタイルを制御するためのキャプション。 ``cfg.irodori_caption`` が設定されている場合はそちらが優先されます。
    :type caption: str | None
    :param ref_wav: スタイル参照に使用する単一のWAVファイルのパス。 ``cfg.irodori_ref_wav`` が設定されている場合はそちらが優先されます。
    :type ref_wav: str | os.PathLike | None
    :param ref_wavs: スタイル参照に使用する複数のWAVファイルのパス。 ``cfg.irodori_ref_wavs`` が設定されている場合はそちらが優先されます。
    :type ref_wavs: Iterable[str | os.PathLike] | str | None
    :param model: ロード済みのIrodori-TTSランタイムインスタンス。指定されない場合、 ``load_model`` を使って新規にロードされます。
    :type model: irodori_tts.inference_runtime.InferenceRuntime | None
    :param model_id: ロードするIrodori-TTSモデルID (``model`` がNoneの場合にのみ使用されます)。 ``cfg.irodori_model_id`` が設定されている場合はそちらが優先されます。
    :type model_id: str
    :param device: モデルの推論に使用するデバイス (``model`` がNoneの場合にのみ使用されます)。 ``cfg.irodori_device`` が設定されている場合はそちらが優先されます。
    :type device: str
    :param precision: モデルの推論に使用する精度 (``model`` がNoneの場合にのみ使用されます)。 ``cfg.irodori_precision`` が設定されている場合はそちらが優先されます。
    :type precision: str
    :param codec_device: コーデックの推論に使用するデバイス (``model`` がNoneの場合にのみ使用されます)。 ``cfg.irodori_codec_device`` が設定されている場合はそちらが優先されます。
    :type codec_device: str | None
    :param codec_precision: コーデックの推論に使用する精度 (``model`` がNoneの場合にのみ使用されます)。 ``cfg.irodori_codec_precision`` が設定されている場合はそちらが優先されます。
    :type codec_precision: str | None
    :returns: 生成が成功したかを示す真偽値と、生成された一時ファイルのパスのリストのタプル。
    :rtype: tuple[bool, list[str]]
    """
    del outfile  # Kept in the signature for tktts backend compatibility.
    speakers = {} if speakers is None else speakers

    print("tktts_irodori.speak_dialogue(): target_voices:", target_voices)

    model_id = _cfg_value(cfg, "irodori_model_id", model_id)
    device = _cfg_value(cfg, "irodori_device", device)
    precision = _cfg_value(cfg, "irodori_precision", precision)
    codec_device = _cfg_value(cfg, "irodori_codec_device", None)
    codec_precision = _cfg_value(cfg, "irodori_codec_precision", None)

    if model is None:
        try:
            model = load_model(
                model_id=model_id,
                device=device,
                precision=precision,
                codec_device=codec_device,
                codec_precision=codec_precision,
            )
        except Exception as exc:
            print(f"❌ Irodori-TTSモデル読み込みエラー: {exc}")
            return False, []

    effective_caption = (
        caption
        if caption is not None
        else _cfg_value(cfg, "irodori_caption", DEFAULT_CAPTION)
    )
    effective_ref_wav = (
        ref_wav
        if ref_wav is not None
        else _cfg_value(cfg, "irodori_ref_wav", None)
    )
    effective_ref_wavs = (
        ref_wavs
        if ref_wavs is not None
        else _cfg_value(cfg, "irodori_ref_wavs", None)
    )
    num_steps = _cfg_value(cfg, "irodori_num_steps", DEFAULT_NUM_STEPS)
    cfg_scale_text = _cfg_value(
        cfg, "irodori_cfg_scale_text", DEFAULT_CFG_SCALE_TEXT
    )
    cfg_scale_caption = _cfg_value(
        cfg, "irodori_cfg_scale_caption", DEFAULT_CFG_SCALE_CAPTION
    )
    cfg_scale_speaker = _cfg_value(
        cfg, "irodori_cfg_scale_speaker", DEFAULT_CFG_SCALE_SPEAKER
    )
    duration_scale = _cfg_value(cfg, "irodori_duration_scale", 1.0)
    seed = _cfg_value(cfg, "irodori_seed", DEFAULT_SEED)
    lora_adapter = _cfg_value(cfg, "irodori_lora_adapter", None)

    tmpfiles = []
    voices_info = get_available_voices_info(model)
    idx = 1
    is_monologue = bool(_cfg_value(cfg, "monologue", False))

    for i, dialogue_item in enumerate(dialogue):
        print()
        print(f"Dialogue {i:04d}:")
        dialogue_list = split_dialogue(
            dialogue_item,
            target_voices,
            speakers=speakers,
            default_voice=DEFAULT_IRODORI_VOICE,
            is_monologue=is_monologue,
        )

        for speaker, text in dialogue_list:
            tmpfile = os.path.join(temp_dir, f"tmp_{idx:03d}.{ext}")
            text = apply_replacements(text, replacements)
            if isinstance(target_voices, str):
                speaker = target_voices

            try:
                voices_info, target_voice = resolve_speaker_id(
                    speaker,
                    voices_info=voices_info,
                    model=model,
                )
            except ValueError as exc:
                print(exc)
                return False, tmpfiles

            print(f"  {idx:04d}: voice={speaker} ({target_voice}): {text}")

            generated = speak(
                outfile=tmpfile,
                text=text,
                voice=target_voice,
                speak_rate=_cfg_value(cfg, "fspeak_rate", None),
                speak_pitch=_cfg_value(cfg, "fspeak_pitch", None),
                caption=effective_caption,
                ref_wav=effective_ref_wav,
                ref_wavs=effective_ref_wavs,
                model=model,
                num_steps=num_steps,
                cfg_scale_text=cfg_scale_text,
                cfg_scale_caption=cfg_scale_caption,
                cfg_scale_speaker=cfg_scale_speaker,
                duration_scale=duration_scale,
                seed=seed,
                lora_adapter=lora_adapter,
            )
            if generated is None:
                return False, tmpfiles

            tmpfiles.append(tmpfile)
            idx += 1

    return True, tmpfiles


if __name__ == "__main__":
    list_available_voices()