import os
import re
import json
import subprocess
import threading
import traceback
from pathlib import Path

import edge_tts
import asyncio
import requests
import uuid
from urllib.parse import urljoin

from PySide6.QtCore import SignalInstance
from loguru import logger
from typing import List, Union, Tuple, Dict, Optional
from datetime import datetime
from xml.sax.saxutils import unescape
from edge_tts import submaker, SubMaker
# from edge_tts.submaker import mktimestamp  # 函数可能不存在，我们自己实现
from moviepy.video.tools import subtitles

try:
    from moviepy import AudioFileClip

    MOVIEPY_AVAILABLE = True
except ImportError:
    MOVIEPY_AVAILABLE = False
    logger.warning("moviepy 未安装，将使用估算方法计算音频时长")
import time

from app.config import config
from app.utils import utils

_module_async_loop = None
_loop_lock = threading.Lock()


def mktimestamp(time_seconds: float) -> str:
    """
    将秒数转换为 SRT 时间戳格式

    Args:
        time_seconds: 时间（秒）

    Returns:
        str: SRT 格式的时间戳，如 "00:01:23.456"
    """
    hours = int(time_seconds // 3600)
    minutes = int((time_seconds % 3600) // 60)
    seconds = time_seconds % 60
    return f"{hours:02d}:{minutes:02d}:{seconds:06.3f}"


def get_all_azure_voices(filter_locals=None) -> list[str]:
    if filter_locals is None:
        filter_locals = ["zh-CN", "en-US", "zh-HK", "zh-TW", "vi-VN"]
    voices_str = """
Name: af-ZA-AdriNeural
Gender: Female

Name: af-ZA-WillemNeural
Gender: Male

Name: am-ET-AmehaNeural
Gender: Male

Name: am-ET-MekdesNeural
Gender: Female

Name: ar-AE-FatimaNeural
Gender: Female

Name: ar-AE-HamdanNeural
Gender: Male

Name: ar-BH-AliNeural
Gender: Male

Name: ar-BH-LailaNeural
Gender: Female

Name: ar-DZ-AminaNeural
Gender: Female

Name: ar-DZ-IsmaelNeural
Gender: Male

Name: ar-EG-SalmaNeural
Gender: Female

Name: ar-EG-ShakirNeural
Gender: Male

Name: ar-IQ-BasselNeural
Gender: Male

Name: ar-IQ-RanaNeural
Gender: Female

Name: ar-JO-SanaNeural
Gender: Female

Name: ar-JO-TaimNeural
Gender: Male

Name: ar-KW-FahedNeural
Gender: Male

Name: ar-KW-NouraNeural
Gender: Female

Name: ar-LB-LaylaNeural
Gender: Female

Name: ar-LB-RamiNeural
Gender: Male

Name: ar-LY-ImanNeural
Gender: Female

Name: ar-LY-OmarNeural
Gender: Male

Name: ar-MA-JamalNeural
Gender: Male

Name: ar-MA-MounaNeural
Gender: Female

Name: ar-OM-AbdullahNeural
Gender: Male

Name: ar-OM-AyshaNeural
Gender: Female

Name: ar-QA-AmalNeural
Gender: Female

Name: ar-QA-MoazNeural
Gender: Male

Name: ar-SA-HamedNeural
Gender: Male

Name: ar-SA-ZariyahNeural
Gender: Female

Name: ar-SY-AmanyNeural
Gender: Female

Name: ar-SY-LaithNeural
Gender: Male

Name: ar-TN-HediNeural
Gender: Male

Name: ar-TN-ReemNeural
Gender: Female

Name: ar-YE-MaryamNeural
Gender: Female

Name: ar-YE-SalehNeural
Gender: Male

Name: az-AZ-BabekNeural
Gender: Male

Name: az-AZ-BanuNeural
Gender: Female

Name: bg-BG-BorislavNeural
Gender: Male

Name: bg-BG-KalinaNeural
Gender: Female

Name: bn-BD-NabanitaNeural
Gender: Female

Name: bn-BD-PradeepNeural
Gender: Male

Name: bn-IN-BashkarNeural
Gender: Male

Name: bn-IN-TanishaaNeural
Gender: Female

Name: bs-BA-GoranNeural
Gender: Male

Name: bs-BA-VesnaNeural
Gender: Female

Name: ca-ES-EnricNeural
Gender: Male

Name: ca-ES-JoanaNeural
Gender: Female

Name: cs-CZ-AntoninNeural
Gender: Male

Name: cs-CZ-VlastaNeural
Gender: Female

Name: cy-GB-AledNeural
Gender: Male

Name: cy-GB-NiaNeural
Gender: Female

Name: da-DK-ChristelNeural
Gender: Female

Name: da-DK-JeppeNeural
Gender: Male

Name: de-AT-IngridNeural
Gender: Female

Name: de-AT-JonasNeural
Gender: Male

Name: de-CH-JanNeural
Gender: Male

Name: de-CH-LeniNeural
Gender: Female

Name: de-DE-AmalaNeural
Gender: Female

Name: de-DE-ConradNeural
Gender: Male

Name: de-DE-FlorianMultilingualNeural
Gender: Male

Name: de-DE-KatjaNeural
Gender: Female

Name: de-DE-KillianNeural
Gender: Male

Name: de-DE-SeraphinaMultilingualNeural
Gender: Female

Name: el-GR-AthinaNeural
Gender: Female

Name: el-GR-NestorasNeural
Gender: Male

Name: en-AU-NatashaNeural
Gender: Female

Name: en-AU-WilliamNeural
Gender: Male

Name: en-CA-ClaraNeural
Gender: Female

Name: en-CA-LiamNeural
Gender: Male

Name: en-GB-LibbyNeural
Gender: Female

Name: en-GB-MaisieNeural
Gender: Female

Name: en-GB-RyanNeural
Gender: Male

Name: en-GB-SoniaNeural
Gender: Female

Name: en-GB-ThomasNeural
Gender: Male

Name: en-HK-SamNeural
Gender: Male

Name: en-HK-YanNeural
Gender: Female

Name: en-IE-ConnorNeural
Gender: Male

Name: en-IE-EmilyNeural
Gender: Female

Name: en-IN-NeerjaExpressiveNeural
Gender: Female

Name: en-IN-NeerjaNeural
Gender: Female

Name: en-IN-PrabhatNeural
Gender: Male

Name: en-KE-AsiliaNeural
Gender: Female

Name: en-KE-ChilembaNeural
Gender: Male

Name: en-NG-AbeoNeural
Gender: Male

Name: en-NG-EzinneNeural
Gender: Female

Name: en-NZ-MitchellNeural
Gender: Male

Name: en-NZ-MollyNeural
Gender: Female

Name: en-PH-JamesNeural
Gender: Male

Name: en-PH-RosaNeural
Gender: Female

Name: en-SG-LunaNeural
Gender: Female

Name: en-SG-WayneNeural
Gender: Male

Name: en-TZ-ElimuNeural
Gender: Male

Name: en-TZ-ImaniNeural
Gender: Female

Name: en-US-AnaNeural
Gender: Female

Name: en-US-AndrewNeural
Gender: Male

Name: en-US-AriaNeural
Gender: Female

Name: en-US-AvaNeural
Gender: Female

Name: en-US-BrianNeural
Gender: Male

Name: en-US-ChristopherNeural
Gender: Male

Name: en-US-EmmaNeural
Gender: Female

Name: en-US-EricNeural
Gender: Male

Name: en-US-GuyNeural
Gender: Male

Name: en-US-JennyNeural
Gender: Female

Name: en-US-MichelleNeural
Gender: Female

Name: en-US-RogerNeural
Gender: Male

Name: en-US-SteffanNeural
Gender: Male

Name: en-ZA-LeahNeural
Gender: Female

Name: en-ZA-LukeNeural
Gender: Male

Name: es-AR-ElenaNeural
Gender: Female

Name: es-AR-TomasNeural
Gender: Male

Name: es-BO-MarceloNeural
Gender: Male

Name: es-BO-SofiaNeural
Gender: Female

Name: es-CL-CatalinaNeural
Gender: Female

Name: es-CL-LorenzoNeural
Gender: Male

Name: es-CO-GonzaloNeural
Gender: Male

Name: es-CO-SalomeNeural
Gender: Female

Name: es-CR-JuanNeural
Gender: Male

Name: es-CR-MariaNeural
Gender: Female

Name: es-CU-BelkysNeural
Gender: Female

Name: es-CU-ManuelNeural
Gender: Male

Name: es-DO-EmilioNeural
Gender: Male

Name: es-DO-RamonaNeural
Gender: Female

Name: es-EC-AndreaNeural
Gender: Female

Name: es-EC-LuisNeural
Gender: Male

Name: es-ES-AlvaroNeural
Gender: Male

Name: es-ES-ElviraNeural
Gender: Female

Name: es-ES-XimenaNeural
Gender: Female

Name: es-GQ-JavierNeural
Gender: Male

Name: es-GQ-TeresaNeural
Gender: Female

Name: es-GT-AndresNeural
Gender: Male

Name: es-GT-MartaNeural
Gender: Female

Name: es-HN-CarlosNeural
Gender: Male

Name: es-HN-KarlaNeural
Gender: Female

Name: es-MX-DaliaNeural
Gender: Female

Name: es-MX-JorgeNeural
Gender: Male

Name: es-NI-FedericoNeural
Gender: Male

Name: es-NI-YolandaNeural
Gender: Female

Name: es-PA-MargaritaNeural
Gender: Female

Name: es-PA-RobertoNeural
Gender: Male

Name: es-PE-AlexNeural
Gender: Male

Name: es-PE-CamilaNeural
Gender: Female

Name: es-PR-KarinaNeural
Gender: Female

Name: es-PR-VictorNeural
Gender: Male

Name: es-PY-MarioNeural
Gender: Male

Name: es-PY-TaniaNeural
Gender: Female

Name: es-SV-LorenaNeural
Gender: Female

Name: es-SV-RodrigoNeural
Gender: Male

Name: es-US-AlonsoNeural
Gender: Male

Name: es-US-PalomaNeural
Gender: Female

Name: es-UY-MateoNeural
Gender: Male

Name: es-UY-ValentinaNeural
Gender: Female

Name: es-VE-PaolaNeural
Gender: Female

Name: es-VE-SebastianNeural
Gender: Male

Name: et-EE-AnuNeural
Gender: Female

Name: et-EE-KertNeural
Gender: Male

Name: fa-IR-DilaraNeural
Gender: Female

Name: fa-IR-FaridNeural
Gender: Male

Name: fi-FI-HarriNeural
Gender: Male

Name: fi-FI-NooraNeural
Gender: Female

Name: fil-PH-AngeloNeural
Gender: Male

Name: fil-PH-BlessicaNeural
Gender: Female

Name: fr-BE-CharlineNeural
Gender: Female

Name: fr-BE-GerardNeural
Gender: Male

Name: fr-CA-AntoineNeural
Gender: Male

Name: fr-CA-JeanNeural
Gender: Male

Name: fr-CA-SylvieNeural
Gender: Female

Name: fr-CA-ThierryNeural
Gender: Male

Name: fr-CH-ArianeNeural
Gender: Female

Name: fr-CH-FabriceNeural
Gender: Male

Name: fr-FR-DeniseNeural
Gender: Female

Name: fr-FR-EloiseNeural
Gender: Female

Name: fr-FR-HenriNeural
Gender: Male

Name: fr-FR-RemyMultilingualNeural
Gender: Male

Name: fr-FR-VivienneMultilingualNeural
Gender: Female

Name: ga-IE-ColmNeural
Gender: Male

Name: ga-IE-OrlaNeural
Gender: Female

Name: gl-ES-RoiNeural
Gender: Male

Name: gl-ES-SabelaNeural
Gender: Female

Name: gu-IN-DhwaniNeural
Gender: Female

Name: gu-IN-NiranjanNeural
Gender: Male

Name: he-IL-AvriNeural
Gender: Male

Name: he-IL-HilaNeural
Gender: Female

Name: hi-IN-MadhurNeural
Gender: Male

Name: hi-IN-SwaraNeural
Gender: Female

Name: hr-HR-GabrijelaNeural
Gender: Female

Name: hr-HR-SreckoNeural
Gender: Male

Name: hu-HU-NoemiNeural
Gender: Female

Name: hu-HU-TamasNeural
Gender: Male

Name: id-ID-ArdiNeural
Gender: Male

Name: id-ID-GadisNeural
Gender: Female

Name: is-IS-GudrunNeural
Gender: Female

Name: is-IS-GunnarNeural
Gender: Male

Name: it-IT-DiegoNeural
Gender: Male

Name: it-IT-ElsaNeural
Gender: Female

Name: it-IT-GiuseppeNeural
Gender: Male

Name: it-IT-IsabellaNeural
Gender: Female

Name: ja-JP-KeitaNeural
Gender: Male

Name: ja-JP-NanamiNeural
Gender: Female

Name: jv-ID-DimasNeural
Gender: Male

Name: jv-ID-SitiNeural
Gender: Female

Name: ka-GE-EkaNeural
Gender: Female

Name: ka-GE-GiorgiNeural
Gender: Male

Name: kk-KZ-AigulNeural
Gender: Female

Name: kk-KZ-DauletNeural
Gender: Male

Name: km-KH-PisethNeural
Gender: Male

Name: km-KH-SreymomNeural
Gender: Female

Name: kn-IN-GaganNeural
Gender: Male

Name: kn-IN-SapnaNeural
Gender: Female

Name: ko-KR-HyunsuNeural
Gender: Male

Name: ko-KR-InJoonNeural
Gender: Male

Name: ko-KR-SunHiNeural
Gender: Female

Name: lo-LA-ChanthavongNeural
Gender: Male

Name: lo-LA-KeomanyNeural
Gender: Female

Name: lt-LT-LeonasNeural
Gender: Male

Name: lt-LT-OnaNeural
Gender: Female

Name: lv-LV-EveritaNeural
Gender: Female

Name: lv-LV-NilsNeural
Gender: Male

Name: mk-MK-AleksandarNeural
Gender: Male

Name: mk-MK-MarijaNeural
Gender: Female

Name: ml-IN-MidhunNeural
Gender: Male

Name: ml-IN-SobhanaNeural
Gender: Female

Name: mn-MN-BataaNeural
Gender: Male

Name: mn-MN-YesuiNeural
Gender: Female

Name: mr-IN-AarohiNeural
Gender: Female

Name: mr-IN-ManoharNeural
Gender: Male

Name: ms-MY-OsmanNeural
Gender: Male

Name: ms-MY-YasminNeural
Gender: Female

Name: mt-MT-GraceNeural
Gender: Female

Name: mt-MT-JosephNeural
Gender: Male

Name: my-MM-NilarNeural
Gender: Female

Name: my-MM-ThihaNeural
Gender: Male

Name: nb-NO-FinnNeural
Gender: Male

Name: nb-NO-PernilleNeural
Gender: Female

Name: ne-NP-HemkalaNeural
Gender: Female

Name: ne-NP-SagarNeural
Gender: Male

Name: nl-BE-ArnaudNeural
Gender: Male

Name: nl-BE-DenaNeural
Gender: Female

Name: nl-NL-ColetteNeural
Gender: Female

Name: nl-NL-FennaNeural
Gender: Female

Name: nl-NL-MaartenNeural
Gender: Male

Name: pl-PL-MarekNeural
Gender: Male

Name: pl-PL-ZofiaNeural
Gender: Female

Name: ps-AF-GulNawazNeural
Gender: Male

Name: ps-AF-LatifaNeural
Gender: Female

Name: pt-BR-AntonioNeural
Gender: Male

Name: pt-BR-FranciscaNeural
Gender: Female

Name: pt-BR-ThalitaNeural
Gender: Female

Name: pt-PT-DuarteNeural
Gender: Male

Name: pt-PT-RaquelNeural
Gender: Female

Name: ro-RO-AlinaNeural
Gender: Female

Name: ro-RO-EmilNeural
Gender: Male

Name: ru-RU-DmitryNeural
Gender: Male

Name: ru-RU-SvetlanaNeural
Gender: Female

Name: si-LK-SameeraNeural
Gender: Male

Name: si-LK-ThiliniNeural
Gender: Female

Name: sk-SK-LukasNeural
Gender: Male

Name: sk-SK-ViktoriaNeural
Gender: Female

Name: sl-SI-PetraNeural
Gender: Female

Name: sl-SI-RokNeural
Gender: Male

Name: so-SO-MuuseNeural
Gender: Male

Name: so-SO-UbaxNeural
Gender: Female

Name: sq-AL-AnilaNeural
Gender: Female

Name: sq-AL-IlirNeural
Gender: Male

Name: sr-RS-NicholasNeural
Gender: Male

Name: sr-RS-SophieNeural
Gender: Female

Name: su-ID-JajangNeural
Gender: Male

Name: su-ID-TutiNeural
Gender: Female

Name: sv-SE-MattiasNeural
Gender: Male

Name: sv-SE-SofieNeural
Gender: Female

Name: sw-KE-RafikiNeural
Gender: Male

Name: sw-KE-ZuriNeural
Gender: Female

Name: sw-TZ-DaudiNeural
Gender: Male

Name: sw-TZ-RehemaNeural
Gender: Female

Name: ta-IN-PallaviNeural
Gender: Female

Name: ta-IN-ValluvarNeural
Gender: Male

Name: ta-LK-KumarNeural
Gender: Male

Name: ta-LK-SaranyaNeural
Gender: Female

Name: ta-MY-KaniNeural
Gender: Female

Name: ta-MY-SuryaNeural
Gender: Male

Name: ta-SG-AnbuNeural
Gender: Male

Name: ta-SG-VenbaNeural
Gender: Female

Name: te-IN-MohanNeural
Gender: Male

Name: te-IN-ShrutiNeural
Gender: Female

Name: th-TH-NiwatNeural
Gender: Male

Name: th-TH-PremwadeeNeural
Gender: Female

Name: tr-TR-AhmetNeural
Gender: Male

Name: tr-TR-EmelNeural
Gender: Female

Name: uk-UA-OstapNeural
Gender: Male

Name: uk-UA-PolinaNeural
Gender: Female

Name: ur-IN-GulNeural
Gender: Female

Name: ur-IN-SalmanNeural
Gender: Male

Name: ur-PK-AsadNeural
Gender: Male

Name: ur-PK-UzmaNeural
Gender: Female

Name: uz-UZ-MadinaNeural
Gender: Female

Name: uz-UZ-SardorNeural
Gender: Male

Name: vi-VN-HoaiMyNeural
Gender: Female

Name: vi-VN-NamMinhNeural
Gender: Male

Name: zh-CN-XiaoxiaoNeural
Gender: Female

Name: zh-CN-XiaoyiNeural
Gender: Female

Name: zh-CN-YunjianNeural
Gender: Male

Name: zh-CN-YunxiNeural
Gender: Male

Name: zh-CN-YunxiaNeural
Gender: Male

Name: zh-CN-YunyangNeural
Gender: Male

Name: zh-CN-liaoning-XiaobeiNeural
Gender: Female

Name: zh-CN-shaanxi-XiaoniNeural
Gender: Female

Name: zh-HK-HiuGaaiNeural
Gender: Female

Name: zh-HK-HiuMaanNeural
Gender: Female

Name: zh-HK-WanLungNeural
Gender: Male

Name: zh-TW-HsiaoChenNeural
Gender: Female

Name: zh-TW-HsiaoYuNeural
Gender: Female

Name: zh-TW-YunJheNeural
Gender: Male

Name: zu-ZA-ThandoNeural
Gender: Female

Name: zu-ZA-ThembaNeural
Gender: Male


Name: en-US-AvaMultilingualNeural-V2
Gender: Female

Name: en-US-AndrewMultilingualNeural-V2
Gender: Male

Name: en-US-EmmaMultilingualNeural-V2
Gender: Female

Name: en-US-BrianMultilingualNeural-V2
Gender: Male

Name: de-DE-FlorianMultilingualNeural-V2
Gender: Male

Name: de-DE-SeraphinaMultilingualNeural-V2
Gender: Female

Name: fr-FR-RemyMultilingualNeural-V2
Gender: Male

Name: fr-FR-VivienneMultilingualNeural-V2
Gender: Female

Name: zh-CN-XiaoxiaoMultilingualNeural-V2
Gender: Female

Name: zh-CN-YunxiNeural-V2
Gender: Male
    """.strip()
    voices = []
    name = ""
    for line in voices_str.split("\n"):
        line = line.strip()
        if not line:
            continue
        if line.startswith("Name: "):
            name = line[6:].strip()
        if line.startswith("Gender: "):
            gender = line[8:].strip()
            if name and gender:
                # voices.append({
                #     "name": name,
                #     "gender": gender,
                # })
                if filter_locals:
                    for filter_local in filter_locals:
                        if name.lower().startswith(filter_local.lower()):
                            voices.append(f"{name}-{gender}")
                else:
                    voices.append(f"{name}-{gender}")
                name = ""
    voices.sort()
    return voices


def parse_voice_name(name: str):
    # zh-CN-XiaoyiNeural-Female
    # zh-CN-YunxiNeural-Male
    # zh-CN-XiaoxiaoMultilingualNeural-V2-Female
    name = name.replace("-Female", "").replace("-Male", "").strip()
    return name


def is_azure_v2_voice(voice_name: str):
    voice_name = parse_voice_name(voice_name)
    if voice_name.endswith("-V2"):
        return voice_name.replace("-V2", "").strip()
    return ""


def should_use_azure_speech_services(voice_name: str) -> bool:
    """判断音色是否应该使用Azure Speech Services"""
    if not voice_name or is_soulvoice_voice(voice_name):
        return False

    voice_name = voice_name.strip()

    # 如果是带-V2后缀的，肯定是Azure Speech Services
    if voice_name.endswith("-V2"):
        return True

    # 检查是否为Azure官方音色格式 (如: zh-CN-YunzeNeural)
    # Azure音色通常格式为: [语言]-[地区]-[名称]Neural
    import re
    pattern = r'^[a-z]{2}-[A-Z]{2}-\w+Neural$'
    if re.match(pattern, voice_name):
        return True

    return False


def tts(
        text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str, tts_engine: str,
        voice_volume: str = ""
) -> Union[SubMaker, None]:
    logger.info(f"使用 TTS 引擎: '{tts_engine}', 语音: '{voice_name}'")

    if tts_engine == "tencent_tts":
        logger.info("分发到腾讯云 TTS")
        return tencent_tts(text, voice_name, voice_file, speed=voice_rate)

    if tts_engine == "soulvoice":
        logger.info("分发到 SoulVoice TTS")
        return soulvoice_tts(text, voice_name, voice_file, speed=voice_rate)

    if tts_engine == "azure_speech":
        if should_use_azure_speech_services(voice_name):
            logger.info("分发到 Azure Speech Services (V2)")
            return azure_tts_v2(text, voice_name, voice_file)
        logger.info("分发到 Edge TTS (Azure V1)")
        return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file, voice_volume)

    if tts_engine == "edge_tts":
        logger.info("分发到 Edge TTS")
        return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file, voice_volume)

    # Fallback for unknown engine - default to azure v1
    logger.warning(f"未知的 TTS 引擎: '{tts_engine}', 将默认使用 Edge TTS (Azure V1)。")
    return azure_tts_v1(text, voice_name, voice_rate, voice_pitch, voice_file)


def convert_rate_to_percent(rate: float) -> str:
    if rate == 1.0:
        return "+0%"
    percent = round((rate - 1.0) * 100)
    if percent > 0:
        return f"+{percent}%"
    else:
        return f"{percent}%"


def convert_pitch_to_percent(rate: float) -> str:
    if rate == 1.0:
        return "+0Hz"
    percent = round((rate - 1.0) * 100)
    if percent > 0:
        return f"+{percent}Hz"
    else:
        return f"{percent}Hz"


def convert_volume_to_percent(volume: float) -> int:
    value = float(volume if volume is not None else 1.0)
    if value <= 2:
        value *= 100
    return max(0, min(100, round(value)))


def normalize_cosyvoice_rate(rate: float) -> float:
    value = float(rate if rate is not None else 1.0)
    return max(0.5, min(2.0, value))

def _get_or_create_loop() -> asyncio.AbstractEventLoop:
    global _module_async_loop
    with _loop_lock:
        if _module_async_loop is None or _module_async_loop.is_closed():
            # 初始化新循环，绑定到当前线程（线程隔离）
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            _module_async_loop = loop
    return _module_async_loop


# 3. 自动清理函数（程序退出时调用，避免资源泄漏）
def _cleanup_loop():
    global _module_async_loop
    lock = globals().get("_loop_lock")
    if lock is None:
        return
    with lock:
        loop = globals().get("_module_async_loop")
        if loop and not loop.is_closed():
            # 取消所有未完成的任务
            tasks = asyncio.all_tasks(loop=loop)
            for task in tasks:
                task.cancel()
                try:
                    loop.run_until_complete(task)
                except asyncio.CancelledError:
                    pass
            # 关闭循环
            loop.close()
            _module_async_loop = None
            logger.info("TTS 工具：模块级 EventLoop 已清理")


# 4. 注册程序退出时的清理（关键：避免程序退出时残留资源）
import atexit

atexit.register(_cleanup_loop)  # 程序退出时自动调用 _cleanup_loop


def azure_tts_v1(
        text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str, voice_volume: str = ""
) -> Union[SubMaker, None]:
    voice_name = parse_voice_name(voice_name)
    text = text.strip()
    rate_str = convert_rate_to_percent(voice_rate)
    pitch_str = convert_pitch_to_percent(voice_pitch)
    voice_volume = voice_volume if voice_volume else "+0%"
    logger.info(f"开始生成音频：text={text}, voice={voice_name}, volume={voice_volume}, 文件={voice_file}")

    # 异步核心逻辑（封装为内部协程）
    async def _do_tts() -> tuple[Optional[SubMaker], bytes]:
        communicate: Optional[edge_tts.Communicate] = None
        try:
            communicate = edge_tts.Communicate(
                text, voice_name, rate=rate_str, pitch=pitch_str,
                proxy=config.proxy.get("http"), volume=voice_volume
            )
            sub_maker = edge_tts.SubMaker()
            audio_data = bytes()

            # 迭代音频流
            async for chunk in communicate.stream():
                if chunk["type"] == "audio":
                    audio_data += chunk["data"]
                elif chunk["type"] == "WordBoundary":
                    sub_maker.create_sub((chunk["offset"], chunk["duration"]), chunk["text"])

            # 显式关闭资源（兼容 edge_tts 不同版本）
            if hasattr(communicate, 'close') and callable(communicate.close):
                await communicate.close()
            return sub_maker, audio_data
        except Exception as e:
            logger.error(f"异步 TTS 生成失败: {str(e)}", exc_info=True)
            return None, bytes()
        finally:
            # 手动解除引用，加速 GC 回收
            if communicate:
                del communicate

    # 3 次重试逻辑
    for i in range(3):
        try:
            # 获取线程安全的循环（懒加载初始化）
            loop = _get_or_create_loop()
            logger.info(f"第 {i + 1} 次尝试生成音频（循环状态：正常）")

            # 执行异步逻辑（等待完成）
            sub_maker, audio_data = loop.run_until_complete(_do_tts())
            # 数据有效性校验（兼容 edge_tts 版本差异）
            if not sub_maker or not getattr(sub_maker, 'subs', []) or len(audio_data) < 100:  # 音频数据至少 100 字节才有效
                logger.warning(f"第 {i + 1} 次生成失败：无效数据（字幕/音频为空）")
                if i < 2:
                    time.sleep(1)
                continue

            # 写入音频文件
            with open(voice_file, "wb") as file:
                file.write(audio_data)
            logger.info(f"音频生成成功！保存路径：{voice_file}（大小：{len(audio_data)} 字节）")
            return sub_maker

        # 处理循环相关异常（重置循环）
        except (asyncio.CancelledError, RuntimeError, OSError) as e:
            logger.error(f"第 {i + 1} 次尝试失败：EventLoop 异常 - {str(e)}")
            # 加锁重置循环（避免多线程冲突）
            with _loop_lock:
                global _module_async_loop
                if _module_async_loop and not _module_async_loop.is_closed():
                    _module_async_loop.close()
                _module_async_loop = None  # 标记为 None，下次调用自动重新初始化
            if i < 2:
                time.sleep(1)

        # 处理其他异常（如网络、文件权限）
        except Exception as e:
            logger.error(f"第 {i + 1} 次尝试失败：{str(e)}", exc_info=True)
            if i < 2:
                time.sleep(1)

    logger.error(f"3 次尝试均失败，无法生成音频文件：{voice_file}")
    return None


def azure_tts_v2(text: str, voice_name: str, voice_file: str) -> Union[SubMaker, None]:
    # 直接使用官方音色名称，不需要V2后缀验证
    # Azure Speech Services 的音色名称如: zh-CN-YunzeNeural, en-US-AvaMultilingualNeural
    processed_voice_name = voice_name.strip()
    if not processed_voice_name:
        logger.error(f"invalid voice name: {voice_name} (empty)")
        raise ValueError(f"invalid voice name: {voice_name} (empty)")
    text = text.strip()

    # 检查Azure Speech SDK是否可用
    try:
        import azure.cognitiveservices.speech as speechsdk
    except ImportError as e:
        logger.error("Azure Speech SDK 未安装。请运行: pip install azure-cognitiveservices-speech")
        logger.error("或者使用 Edge TTS 引擎作为替代方案")
        return None

    def _format_duration_to_offset(duration) -> int:
        if isinstance(duration, str):
            time_obj = datetime.strptime(duration, "%H:%M:%S.%f")
            milliseconds = (
                    (time_obj.hour * 3600000)
                    + (time_obj.minute * 60000)
                    + (time_obj.second * 1000)
                    + (time_obj.microsecond // 1000)
            )
            return milliseconds * 10000

        if isinstance(duration, int):
            return duration

        return 0

    for i in range(3):
        try:
            logger.info(f"start, voice name: {processed_voice_name}, try: {i + 1}")

            sub_maker = SubMaker()

            def speech_synthesizer_word_boundary_cb(evt: speechsdk.SessionEventArgs):
                duration = _format_duration_to_offset(str(evt.duration))
                offset = _format_duration_to_offset(evt.audio_offset)
                sub_maker.subs.append(evt.text)
                sub_maker.offset.append((offset, offset + duration))

            # Creates an instance of a speech config with specified subscription key and service region.
            speech_key = config.azure.get("speech_key", "")
            service_region = config.azure.get("speech_region", "")
            audio_config = speechsdk.audio.AudioOutputConfig(
                filename=voice_file, use_default_speaker=True
            )
            speech_config = speechsdk.SpeechConfig(
                subscription=speech_key, region=service_region
            )
            speech_config.speech_synthesis_voice_name = processed_voice_name
            # speech_config.set_property(property_id=speechsdk.PropertyId.SpeechServiceResponse_RequestSentenceBoundary,
            #                            value='true')
            speech_config.set_property(
                property_id=speechsdk.PropertyId.SpeechServiceResponse_RequestWordBoundary,
                value="true",
            )

            speech_config.set_speech_synthesis_output_format(
                speechsdk.SpeechSynthesisOutputFormat.Audio48Khz192KBitRateMonoMp3
            )
            speech_synthesizer = speechsdk.SpeechSynthesizer(
                audio_config=audio_config, speech_config=speech_config
            )
            speech_synthesizer.synthesis_word_boundary.connect(
                speech_synthesizer_word_boundary_cb
            )

            result = speech_synthesizer.speak_text_async(text).get()
            if result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
                logger.success(f"azure v2 speech synthesis succeeded: {voice_file}")
                return sub_maker
            elif result.reason == speechsdk.ResultReason.Canceled:
                cancellation_details = result.cancellation_details
                logger.error(
                    f"azure v2 speech synthesis canceled: {cancellation_details.reason}"
                )
                if cancellation_details.reason == speechsdk.CancellationReason.Error:
                    logger.error(
                        f"azure v2 speech synthesis error: {cancellation_details.error_details}"
                    )
            if i < 2:  # 如果不是最后一次重试，则等待1秒
                time.sleep(1)
            logger.info(f"completed, output file: {voice_file}")
        except Exception as e:
            logger.error(f"failed, error: {str(e)}")
            if i < 2:  # 如果不是最后一次重试，则等待1秒
                time.sleep(3)
    return None


def _format_text(text: str) -> str:
    text = text.replace("\n", " ")
    text = text.replace("\"", " ")
    text = text.replace("[", " ")
    text = text.replace("]", " ")
    text = text.replace("(", " ")
    text = text.replace(")", " ")
    text = text.replace("）", " ")
    text = text.replace("（", " ")
    text = text.replace("{", " ")
    text = text.replace("}", " ")
    text = text.strip()
    return text


def create_subtitle_from_multiple(text: str, sub_maker_list: List[SubMaker], list_script: List[dict],
                                  subtitle_file: str):
    """
    根据多个 SubMaker 对象、完整文本和原始脚本创建优化的字幕文件
    1. 使用原始脚本中的时间戳
    2. 跳过 OST 为 true 的部分
    3. 将字幕文件按照标点符号分割成多行
    4. 根据完整文本分段，保持原文的语句结构
    5. 生成新的字幕文件，时间戳包含小时单位
    """
    text = _format_text(text)
    sentences = utils.split_string_by_punctuations(text)

    def formatter(idx: int, start_time: str, end_time: str, sub_text: str) -> str:
        return f"{idx}\n{start_time.replace('.', ',')} --> {end_time.replace('.', ',')}\n{sub_text}\n"

    sub_items = []
    sub_index = 0
    sentence_index = 0

    try:
        sub_maker_index = 0
        for script_item in list_script:
            if script_item['OST']:
                continue

            start_time, end_time = script_item['timestamp'].split('-')
            if sub_maker_index >= len(sub_maker_list):
                logger.error(f"Sub maker list index out of range: {sub_maker_index}")
                break
            sub_maker = sub_maker_list[sub_maker_index]
            sub_maker_index += 1

            script_duration = utils.time_to_seconds(end_time) - utils.time_to_seconds(start_time)
            audio_duration = get_audio_duration(sub_maker)
            time_ratio = script_duration / audio_duration if audio_duration > 0 else 1

            current_sub = ""
            current_start = None
            current_end = None

            for offset, sub in zip(sub_maker.offset, sub_maker.subs):
                sub = unescape(sub).strip()
                sub_start = utils.seconds_to_time(utils.time_to_seconds(start_time) + offset[0] / 10000000 * time_ratio)
                sub_end = utils.seconds_to_time(utils.time_to_seconds(start_time) + offset[1] / 10000000 * time_ratio)

                if current_start is None:
                    current_start = sub_start
                current_end = sub_end

                current_sub += sub

                # 检查当前累积的字幕是否匹配下一个句子
                while sentence_index < len(sentences) and sentences[sentence_index] in current_sub:
                    sub_index += 1
                    line = formatter(
                        idx=sub_index,
                        start_time=current_start,
                        end_time=current_end,
                        sub_text=sentences[sentence_index].strip(),
                    )
                    sub_items.append(line)
                    current_sub = current_sub.replace(sentences[sentence_index], "", 1).strip()
                    current_start = current_end
                    sentence_index += 1

                # 如果当前字幕长度超过15个字符，也生成一个新的字幕项
                if len(current_sub) > 15:
                    sub_index += 1
                    line = formatter(
                        idx=sub_index,
                        start_time=current_start,
                        end_time=current_end,
                        sub_text=current_sub.strip(),
                    )
                    sub_items.append(line)
                    current_sub = ""
                    current_start = current_end

            # 处理剩余的文本
            if current_sub.strip():
                sub_index += 1
                line = formatter(
                    idx=sub_index,
                    start_time=current_start,
                    end_time=current_end,
                    sub_text=current_sub.strip(),
                )
                sub_items.append(line)

        if len(sub_items) == 0:
            logger.error("No subtitle items generated")
            return

        with open(subtitle_file, "w", encoding="utf-8") as file:
            file.write("\n".join(sub_items))

        logger.info(f"completed, subtitle file created: {subtitle_file}")
    except Exception as e:
        logger.error(f"failed, error: {str(e)}")
        traceback.print_exc()


def create_subtitle(sub_maker: submaker.SubMaker, text: str, subtitle_file: str):
    """
    优化字幕文件
    1. 将字幕文件按照标点符号分割成多行
    2. 逐行匹配字幕文件中的文本
    3. 生成新的字幕文件
    """

    text = _format_text(text)

    def formatter(idx: int, start_time: float, end_time: float, sub_text: str) -> str:
        """
        1
        00:00:00,000 --> 00:00:02,360
        跑步是一项简单易行的运动
        """
        start_t = mktimestamp(start_time).replace(".", ",")
        end_t = mktimestamp(end_time).replace(".", ",")
        return f"{idx}\n" f"{start_t} --> {end_t}\n" f"{sub_text}\n"

    start_time = -1.0
    sub_items = []
    sub_index = 0
    print(text)
    script_lines = utils.split_string_by_punctuations(text)
    print(script_lines)

    def match_line(_sub_line: str, _sub_index: int):
        if len(script_lines) <= _sub_index:
            return ""

        _line = script_lines[_sub_index]
        if _sub_line == _line:
            return script_lines[_sub_index].strip()

        _sub_line_ = re.sub(r"[^\w\s]", "", _sub_line)
        _line_ = re.sub(r"[^\w\s]", "", _line)
        if _sub_line_ == _line_:
            return _line_.strip()

        _sub_line_ = re.sub(r"\W+", "", _sub_line)
        _line_ = re.sub(r"\W+", "", _line)
        if _sub_line_ == _line_:
            return _line.strip()

        return ""

    sub_line = ""

    try:
        for _, (offset, sub) in enumerate(zip(sub_maker.offset, sub_maker.subs)):
            _start_time, end_time = offset
            if start_time < 0:
                start_time = _start_time

            # 将 100纳秒单位转换为秒
            start_time_seconds = start_time / 10000000
            end_time_seconds = end_time / 10000000

            sub = unescape(sub)
            sub_line += sub
            sub_text = match_line(sub_line, sub_index)
            if sub_text:
                sub_index += 1
                line = formatter(
                    idx=sub_index,
                    start_time=start_time_seconds,
                    end_time=end_time_seconds,
                    sub_text=sub_text,
                )
                sub_items.append(line)
                start_time = -1.0
                sub_line = ""

        if len(sub_items) == len(script_lines) - 1:
            if script_lines[len(script_lines) - 1] == '”':
                script_lines.pop(len(script_lines) - 1)
        if len(sub_items) == len(script_lines):
            with open(subtitle_file, "w", encoding="utf-8") as file:
                file.write("\n".join(sub_items) + "\n")
            try:
                sbs = subtitles.file_to_subtitles(subtitle_file, encoding="utf-8")
                duration = max([tb for ((ta, tb), txt) in sbs])
                logger.info(
                    f"已创建字幕文件: {subtitle_file}, duration: {duration}"
                )
                return subtitle_file, duration
            except Exception as e:
                logger.error(f"failed, error: {str(e)}")
                os.remove(subtitle_file)
        else:
            logger.error(
                f"字幕创建失败, 字幕长度: {len(sub_items)}, script_lines len: {len(script_lines)}"
                f"\nsub_items:{json.dumps(sub_items, indent=4, ensure_ascii=False)}"
                f"\nscript_lines:{json.dumps(script_lines, indent=4, ensure_ascii=False)}"
            )
            # 返回默认值，避免 None 错误
            return subtitle_file, 3.0

    except Exception as e:
        logger.error(f"failed, error: {str(e)}")
        # 返回默认值，避免 None 错误
        return subtitle_file, 3.0


def get_audio_duration(sub_maker: submaker.SubMaker):
    """
    获取音频时长
    """
    if not sub_maker.offset:
        return 0.0
    return sub_maker.offset[-1][1] / 10000000


def tts_multiple(output_dir: str, list_script: list, voice_name: str, voice_rate: float, voice_pitch: float,
                 tts_engine: str = "azure", script_path: str = "", data: Dict = None,
                 cut_progress: SignalInstance = None):
    """
    根据JSON文件中的多段文本进行TTS转换

    :param task_id: 任务ID
    :param list_script: 脚本列表
    :param voice_name: 语音名称
    :param voice_rate: 语音速率
    :param tts_engine: TTS 引擎
    :return: 生成的音频文件列表
    """
    voice_name = parse_voice_name(voice_name)
    tts_results = []
    new_list_script = list_script.copy()
    for index, item in enumerate(list_script):
        if item['OST'] != 1:
            # 将时间戳中的冒号替换为下划线
            timestamp = item['timestamp'].replace(':', '_')
            tts_path = output_dir + "/tts"
            Path(tts_path).mkdir(parents=True, exist_ok=True)
            audio_file = os.path.join(tts_path, f"audio_{timestamp}.mp3")
            subtitle_file = os.path.join(tts_path, f"subtitle_{timestamp}.srt")
            text = ""
            if "text" in item:
                text = item['text']
            if text == "":
                text = item['narration']
            is_selected = True
            if "is_selected" in item:
                is_selected = item['is_selected']
            change = False
            if "change" in item:
                change = item['change']
            if is_selected and (change or not os.path.exists(audio_file)):
                new_list_script[index]['audio_file'] = ''
                new_list_script[index]['subtitle_file'] = ''
                new_list_script[index]['duration'] = 0

                sub_maker = tts(
                    text=text,
                    voice_name=voice_name,
                    voice_rate=voice_rate,
                    voice_pitch=voice_pitch,
                    voice_file=audio_file,
                    tts_engine=tts_engine,
                )

                if sub_maker is None:
                    logger.error(f"无法为时间戳 {timestamp} 生成音频; "
                                 f"如果您在中国，请使用VPN; "
                                 f"或者使用其他 tts 引擎")
                    continue
                else:
                    # SoulVoice 引擎不生成字幕文件
                    if is_soulvoice_voice(voice_name):
                        # 获取实际音频文件的时长
                        duration = get_audio_duration_from_file(audio_file)
                        if duration <= 0:
                            # 如果无法获取文件时长，尝试从 SubMaker 获取
                            duration = get_audio_duration(sub_maker)
                            if duration <= 0:
                                # 最后的 fallback，基于文本长度估算
                                duration = max(1.0, len(text) / 3.0)
                                logger.warning(f"无法获取音频时长，使用文本估算: {duration:.2f}秒")
                        # 不创建字幕文件
                        subtitle_file = ""
                    else:
                        _, duration = create_subtitle(sub_maker=sub_maker, text=text, subtitle_file=subtitle_file)
            else:
                duration = get_audio_duration_from_file(audio_file)
            tts_results.append({
                "_id": item['_id'],
                "timestamp": item['timestamp'],
                "audio_file": audio_file,
                "subtitle_file": subtitle_file,
                "duration": duration,
                "text": text,
            })
            new_list_script[index]['audio_file'] = audio_file
            new_list_script[index]['subtitle_file'] = subtitle_file
            new_list_script[index]['duration'] = duration
            new_list_script[index]['text'] = text
            new_list_script[index]['change'] = False
            new_list_script[index]['video_change'] = True
            logger.info(f"已生成音频文件: {audio_file}")
        if cut_progress:
            cut_progress.emit(f"生成tts音频 总计{len(list_script)}个，已生成{index + 1}个")
    print(f'tts_results: {tts_results}')
    print(f'new_list_script: {new_list_script}')
    os.remove(script_path)
    with open(script_path, "w", encoding="utf-8") as file:
        json.dump(new_list_script, file, indent=4, ensure_ascii=False)
    return tts_results


def tts_one(text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str = "",
            tts_engine: str = "azure", voice_volume: str = ""):
    if os.path.exists(voice_file):
        os.remove(voice_file)
    print(f'voice_volume---{voice_volume}')
    sub_maker = tts(
        text=text,
        voice_name=voice_name,
        voice_rate=voice_rate,
        voice_pitch=voice_pitch,
        voice_file=voice_file,
        tts_engine=tts_engine,
        voice_volume=voice_volume
    )
    if sub_maker is None:
        logger.error(f"无法生成音频; "
                     f"如果您在中国，请使用VPN; "
                     f"或者使用其他 tts 引擎")
        return 0
    else:
        # SoulVoice 引擎不生成字幕文件
        if is_soulvoice_voice(voice_name):
            # 获取实际音频文件的时长
            duration = get_audio_duration_from_file(voice_file)
            if duration <= 0:
                # 如果无法获取文件时长，尝试从 SubMaker 获取
                duration = get_audio_duration(sub_maker)
                if duration <= 0:
                    # 最后的 fallback，基于文本长度估算
                    duration = max(1.0, len(text) / 3.0)
                    logger.warning(f"无法获取音频时长，使用文本估算: {duration:.2f}秒")
            # 不创建字幕文件
            subtitle_file = ""
        else:
            subtitle_file = voice_file.replace(".mp3", ".srt").replace("audio_", "subtitle_")
            _, duration = create_subtitle(sub_maker=sub_maker, text=text, subtitle_file=subtitle_file)
        return duration


# def get_audio_duration_from_file(audio_file: str) -> float:
#     """
#     获取音频文件的时长（秒）
#     """
#     if MOVIEPY_AVAILABLE:
#         try:
#             audio_clip = AudioFileClip(audio_file)
#             duration = audio_clip.duration
#             audio_clip.close()
#             return duration
#         except Exception as e:
#             logger.error(f"使用 moviepy 获取音频时长失败: {str(e)}")
#
#     # Fallback: 使用更准确的估算方法
#     try:
#         import os
#         file_size = os.path.getsize(audio_file)
#
#         # 更准确的 MP3 时长估算
#         # 假设 MP3 平均比特率为 128kbps = 16KB/s
#         # 但实际文件还包含头部信息，所以调整系数
#         estimated_duration = max(1.0, file_size / 20000)  # 调整为更保守的估算
#
#         # 对于中文语音，根据文本长度进行二次校正
#         # 一般中文语音速度约为 3-4 字/秒
#         logger.warning(f"使用文件大小估算音频时长: {estimated_duration:.2f}秒")
#         return estimated_duration
#     except Exception as e:
#         logger.error(f"获取音频时长失败: {str(e)}")
#         # 如果所有方法都失败，返回一个基于文本长度的估算
#         return 3.0  # 默认3秒，避免返回0
def get_audio_duration_from_file(media_path: str) -> float:
    try:
        cmd = (
            f'ffprobe -v error -select_streams a:0 '
            f'-show_entries stream=duration '
            f'-of default=noprint_wrappers=1:nokey=1 '
            f'"{media_path}"'
        )
        result = subprocess.run(
            cmd, shell=True, capture_output=True, text=True, timeout=30
        )
        duration = float(result.stdout.strip()) if result.stdout.strip() else 0.0
        return duration
    except Exception as e:
        print(f"获取媒体时长失败（{media_path}）：{e}")
        return 0.0


def _download_file(url: str, output_path: str, base_url: str = ""):
    if not url.startswith(("http://", "https://")):
        url = urljoin(base_url, url.lstrip("/"))
    response = requests.get(url, timeout=120)
    response.raise_for_status()
    Path(os.path.dirname(output_path)).mkdir(parents=True, exist_ok=True)
    with open(output_path, "wb") as file:
        file.write(response.content)


def _format_srt_time(seconds: float):
    milliseconds = int(max(seconds, 0) * 1000)
    hours = milliseconds // 3600000
    milliseconds %= 3600000
    minutes = milliseconds // 60000
    milliseconds %= 60000
    secs = milliseconds // 1000
    millis = milliseconds % 1000
    return f"{hours:02}:{minutes:02}:{secs:02},{millis:03}"


def _write_fallback_srt(text: str, subtitle_file: str, duration: float):
    Path(os.path.dirname(subtitle_file)).mkdir(parents=True, exist_ok=True)
    with open(subtitle_file, "w", encoding="utf-8") as file:
        file.write(f"1\n00:00:00,000 --> {_format_srt_time(duration)}\n{text}\n")


def cosyvoice_clone_tts(
        text: str,
        voice_id: str,
        audio_file: str,
        subtitle_file: str,
        domestic_interface: str,
        user_id,
        token: str,
        model_name: str = "cosyvoice-v3-flash",
        voice_rate: float = 1.0,
        voice_pitch: float = 1.0,
        voice_volume: float = 1.0,
        mission_id: str = None,
        segment_id: int = 1,
        segment_total: int = 1,
        instruction: str = ""
) -> float:
    mission_id = mission_id or str(uuid.uuid4())
    request_data = {
        "user_id": user_id,
        "token": token,
        "voice_id": voice_id,
        "model_name": model_name,
        "text": text,
        "mission_id": mission_id,
        "segment_id": segment_id,
        "segment_total": segment_total,
        "rate": normalize_cosyvoice_rate(voice_rate),
        "pitch": normalize_cosyvoice_rate(voice_pitch),
        "volume": convert_volume_to_percent(voice_volume),
    }
    if instruction:
        request_data["instruction"] = instruction
    response = requests.post(
        f"{domestic_interface}voice_clone/tts",
        data=request_data,
        timeout=1800
    )
    response.raise_for_status()
    response_data = response.json()
    if response_data.get("status") != 1:
        raise RuntimeError(response_data.get("error") or response_data.get("message") or response_data.get(
            "msg") or "生成CosyVoice配音失败")

    data = response_data.get("data") or {}
    audio_url = data.get("audio_url")
    srt_url = data.get("srt_url")
    if not audio_url:
        raise RuntimeError("生成CosyVoice配音成功但没有返回音频地址")

    _download_file(audio_url, audio_file, domestic_interface)
    if srt_url:
        _download_file(srt_url, subtitle_file, domestic_interface)

    duration = get_audio_duration_from_file(audio_file)
    if duration <= 0:
        duration = max(1.0, len(text) / 3.0)
    if not srt_url:
        _write_fallback_srt(text, subtitle_file, duration)
    return duration


def cosyvoice_tts(
        text: str,
        voice_name: str,
        model_name: str,
        audio_file: str,
        subtitle_file: str,
        domestic_interface: str,
        user_id,
        token: str,
        voice_rate: float = 1.0,
        voice_pitch: float = 1.0,
        voice_volume: float = 1.0,
        mission_id: str = None,
        segment_id: int = 1,
        segment_total: int = 1,
        instruction: str = ""
) -> float:
    mission_id = mission_id or str(uuid.uuid4())
    request_data = {
        "user_id": user_id,
        "token": token,
        "text": text,
        "voice_name": voice_name,
        "model_name": model_name,
        "mission_id": mission_id,
        "segment_id": segment_id,
        "segment_total": segment_total,
        "rate": normalize_cosyvoice_rate(voice_rate),
        "pitch": normalize_cosyvoice_rate(voice_pitch),
        "volume": convert_volume_to_percent(voice_volume),
    }
    if instruction:
        request_data["instruction"] = instruction
    response = requests.post(
        f"{domestic_interface}cosy_voice",
        data=request_data,
        timeout=1800
    )
    response.raise_for_status()
    response_data = response.json()
    if response_data.get("status") != 1:
        raise RuntimeError(response_data.get("error") or response_data.get("message") or response_data.get(
            "msg") or "生成CosyVoice配音失败")

    data = response_data.get("data") or {}
    audio_url = data.get("audio_url")
    srt_url = data.get("srt_url")
    if not audio_url:
        raise RuntimeError("生成CosyVoice配音成功但没有返回音频地址")

    _download_file(audio_url, audio_file, domestic_interface)
    if srt_url:
        _download_file(srt_url, subtitle_file, domestic_interface)

    duration = get_audio_duration_from_file(audio_file)
    if duration <= 0:
        duration = max(1.0, len(text) / 3.0)
    if not srt_url:
        _write_fallback_srt(text, subtitle_file, duration)
    return duration

def parse_soulvoice_voice(voice_name: str) -> str:
    """
    解析 SoulVoice 语音名称
    支持格式：
    - soulvoice:speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr
    - speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr
    """
    if voice_name.startswith("soulvoice:"):
        return voice_name[10:]  # 移除 "soulvoice:" 前缀
    return voice_name


def parse_tencent_voice(voice_name: str) -> str:
    """
    解析腾讯云 TTS 语音名称
    支持格式：tencent:101001
    """
    if voice_name.startswith("tencent:"):
        return voice_name[8:]  # 移除 "tencent:" 前缀
    return voice_name


def tencent_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]:
    """
    使用腾讯云 TTS 生成语音
    """
    try:
        # 导入腾讯云 SDK
        from tencentcloud.common import credential
        from tencentcloud.common.profile.client_profile import ClientProfile
        from tencentcloud.common.profile.http_profile import HttpProfile
        from tencentcloud.tts.v20190823 import tts_client, models
        import base64
    except ImportError as e:
        logger.error(f"腾讯云 SDK 未安装: {e}")
        return None

    # 获取腾讯云配置
    tencent_config = config.tencent
    secret_id = tencent_config.get("secret_id")
    secret_key = tencent_config.get("secret_key")
    region = tencent_config.get("region", "ap-beijing")

    if not secret_id or not secret_key:
        logger.error("腾讯云 TTS 配置不完整，请检查 secret_id 和 secret_key")
        return None

    # 解析语音名称
    voice_type = parse_tencent_voice(voice_name)

    # 转换速度参数 (腾讯云支持 -2 到 2 的范围)
    speed_value = max(-2.0, min(2.0, (speed - 1.0) * 2))

    for i in range(3):
        try:
            logger.info(f"第 {i + 1} 次使用腾讯云 TTS 生成音频")

            # 创建认证对象
            cred = credential.Credential(secret_id, secret_key)

            # 创建 HTTP 配置
            httpProfile = HttpProfile()
            httpProfile.endpoint = "tts.tencentcloudapi.com"

            # 创建客户端配置
            clientProfile = ClientProfile()
            clientProfile.httpProfile = httpProfile

            # 创建客户端
            client = tts_client.TtsClient(cred, region, clientProfile)

            req = models.TextToVoiceRequest()
            req.Text = text
            req.SessionId = str(uuid.uuid4())
            req.VoiceType = int(voice_type) if voice_type.isdigit() else 101001
            req.Speed = speed_value
            req.SampleRate = 16000
            req.Codec = "mp3"
            req.ProjectId = 0
            req.ModelType = 1
            req.PrimaryLanguage = 1
            req.EnableSubtitle = True

            # 发送请求
            resp = client.TextToVoice(req)

            # 检查响应
            if not resp.Audio:
                logger.warning(f"腾讯云 TTS 返回空音频数据")
                if i < 2:
                    time.sleep(1)
                continue

            # 解码音频数据
            audio_data = base64.b64decode(resp.Audio)

            # 写入文件
            with open(voice_file, "wb") as f:
                f.write(audio_data)

            # 创建字幕对象
            sub_maker = SubMaker()
            if resp.Subtitles:
                for sub in resp.Subtitles:
                    start_ms = sub.BeginTime
                    end_ms = sub.EndTime
                    text = sub.Text
                    # 转换为 100ns 单位
                    sub_maker.create_sub((start_ms * 10000, end_ms * 10000), text)
            else:
                # 如果没有字幕返回，则使用估算作为后备方案
                duration_ms = len(text) * 200
                sub_maker.create_sub((0, duration_ms * 10000), text)

            logger.info(f"腾讯云 TTS 生成成功，文件大小: {len(audio_data)} 字节")
            return sub_maker

        except Exception as e:
            logger.error(f"腾讯云 TTS 生成音频时出错: {str(e)}")
            if i < 2:
                time.sleep(1)

    return None


def soulvoice_tts(text: str, voice_name: str, voice_file: str, speed: float = 1.0) -> Union[SubMaker, None]:
    """
    使用 SoulVoice API 进行文本转语音

    Args:
        text: 要转换的文本
        voice_name: 语音名称
        voice_file: 输出音频文件路径
        speed: 语音速度

    Returns:
        SubMaker: 包含时间戳信息的字幕制作器，失败时返回 None
    """
    # 获取配置
    api_key = config.soulvoice.get("api_key", "")
    api_url = config.soulvoice.get("api_url", "https://tts.scsmtech.cn/tts")
    default_model = config.soulvoice.get("model", "FunAudioLLM/CosyVoice2-0.5B")

    if not api_key:
        logger.error("SoulVoice API key 未配置")
        return None

    # 解析语音名称
    parsed_voice = parse_soulvoice_voice(voice_name)

    # 准备请求数据
    headers = {
        'Authorization': f'Bearer {api_key}',
        'Content-Type': 'application/json'
    }

    data = {
        'text': text.strip(),
        'model': default_model,
        'voice': parsed_voice,
        'speed': speed
    }

    # 重试机制
    for attempt in range(3):
        try:
            logger.info(f"第 {attempt + 1} 次调用 SoulVoice API")

            # 设置代理
            proxies = {}
            # if config.proxy.get("http"):
            #     proxies = {
            #         'http': config.proxy.get("http"),
            #         'https': config.proxy.get("https", config.proxy.get("http"))
            #     }

            # 调用 API
            response = requests.post(
                api_url,
                headers=headers,
                json=data,
                proxies=proxies,
                timeout=60
            )

            if response.status_code == 200:
                # 保存音频文件
                with open(voice_file, 'wb') as f:
                    f.write(response.content)

                logger.info(f"SoulVoice TTS 成功生成音频: {voice_file}")

                # SoulVoice 不支持精确字幕生成，返回简单的 SubMaker 对象
                sub_maker = SubMaker()
                sub_maker.subs = [text]  # 整个文本作为一个段落
                sub_maker.offset = [(0, 0)]  # 占位时间戳

                return sub_maker

            else:
                logger.error(f"SoulVoice API 调用失败: {response.status_code} - {response.text}")

        except requests.exceptions.Timeout:
            logger.error(f"SoulVoice API 调用超时 (尝试 {attempt + 1}/3)")
        except requests.exceptions.RequestException as e:
            logger.error(f"SoulVoice API 网络错误: {str(e)} (尝试 {attempt + 1}/3)")
        except Exception as e:
            logger.error(f"SoulVoice TTS 处理错误: {str(e)} (尝试 {attempt + 1}/3)")

        if attempt < 2:  # 不是最后一次尝试
            time.sleep(2)  # 等待2秒后重试

    logger.error("SoulVoice TTS 生成失败，已达到最大重试次数")
    return None


def is_soulvoice_voice(voice_name: str) -> bool:
    """
    检查是否为 SoulVoice 语音
    """
    return voice_name.startswith("soulvoice:") or voice_name.startswith("speech:")


def parse_soulvoice_voice(voice_name: str) -> str:
    """
    解析 SoulVoice 语音名称
    支持格式：
    - soulvoice:speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr
    - speech:mcg3fdnx:clzkyf4vy00e5qr6hywum4u84:bzznlkuhcjzpbosexitr
    """
    if voice_name.startswith("soulvoice:"):
        return voice_name[10:]  # 移除 "soulvoice:" 前缀
    return voice_name
