diff --git a/.gitignore b/.gitignore index e59ea65b5..9ac4f1429 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ venv/* pytest.ini AGENTS.md IFLOW.md + +# genie_tts data +CharacterModels/ +GenieData/ \ No newline at end of file diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 510b162a7..48de57c6e 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -1179,6 +1179,15 @@ CONFIG_METADATA_2 = { "openai-tts-voice": "alloy", "timeout": "20", }, + "Genie TTS": { + "id": "genie_tts", + "provider": "genie_tts", + "type": "genie_tts", + "provider_type": "text_to_speech", + "enable": False, + "character_name": "mika", + "timeout": 20, + }, "Edge TTS": { "id": "edge_tts", "provider": "microsoft", diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index b523a0661..f6db6d87a 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -322,6 +322,10 @@ class ProviderManager: from .sources.openai_tts_api_source import ( ProviderOpenAITTSAPI as ProviderOpenAITTSAPI, ) + case "genie_tts": + from .sources.genie_tts import ( + GenieTTSProvider as GenieTTSProvider, + ) case "edge_tts": from .sources.edge_tts_source import ( ProviderEdgeTTS as ProviderEdgeTTS, @@ -422,17 +426,20 @@ class ProviderManager: except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。", + exc_info=True, ) return except Exception as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。未知原因", + exc_info=True, ) return if provider_config["type"] not in provider_cls_map: logger.error( f"未找到适用于 {provider_config['type']}({provider_config['id']}) 的提供商适配器,请检查是否已经安装或者名称填写错误。已跳过。", + exc_info=True, ) return diff --git a/astrbot/core/provider/sources/genie_tts.py b/astrbot/core/provider/sources/genie_tts.py new file mode 100644 index 000000000..a051742eb --- /dev/null +++ b/astrbot/core/provider/sources/genie_tts.py @@ -0,0 +1,69 @@ +import asyncio +import os +import uuid + +from astrbot.core.provider.entities import ProviderType +from astrbot.core.provider.provider import TTSProvider +from astrbot.core.provider.register import register_provider_adapter +from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +# genie_data_dir = os.path.join(get_astrbot_data_path(), "genie_tts_data") +# os.makedirs(genie_data_dir, exist_ok=True) +# os.environ["GENIE_DATA_DIR"] = genie_data_dir + +try: + import genie_tts as genie # type: ignore +except ImportError: + genie = None + + +@register_provider_adapter( + "genie_tts", + "Genie TTS", + provider_type=ProviderType.TEXT_TO_SPEECH, +) +class GenieTTSProvider(TTSProvider): + def __init__( + self, + provider_config: dict, + provider_settings: dict, + ) -> None: + super().__init__(provider_config, provider_settings) + if not genie: + raise ImportError("Please install genie_tts first.") + + self.character_name = provider_config.get("character_name", "mika") + + # Automatically downloads required files on first run + # This is done synchronously as per the library usage, might block on first run. + try: + genie.load_predefined_character(self.character_name) + except Exception as e: + raise RuntimeError(f"Failed to load character {self.character_name}: {e}") + + async def get_audio(self, text: str) -> str: + temp_dir = os.path.join(get_astrbot_data_path(), "temp") + os.makedirs(temp_dir, exist_ok=True) + filename = f"genie_tts_{uuid.uuid4()}.wav" + path = os.path.join(temp_dir, filename) + + loop = asyncio.get_event_loop() + + def _generate(save_path: str): + assert genie is not None + # Assuming it returns bytes: + genie.tts( + character_name=self.character_name, + text=text, + save_path=save_path, + ) + + try: + await loop.run_in_executor(None, _generate, path) + + if os.path.exists(path): + return path + raise RuntimeError("Genie TTS did not return audio bytes or save to file.") + + except Exception as e: + raise RuntimeError(f"Genie TTS generation failed: {e}")