初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+245
View File
@@ -0,0 +1,245 @@
"""音频服务类型 — 请求/响应模型和协议定义。
ASR(自动语音识别)和 TTS(文本转语音)适配器**不是** LangChain 的
``BaseChatModel`` 子类 —— 它们是通过 HTTP 调用的服务,经由 ``AudioService``
或直接从 ``ModelFactory`` 消费。
用法::
service = AudioService(factory)
result = await service.transcribe(audio_data)
result = await service.synthesize("你好")
# 或通过工厂直接调用:
tts = factory.create_audio("mimo-v2.5-tts")
result = await tts.synthesize(TTSRequest(text="你好"))
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
from app.ai_provider.types.errors import AIProviderError
if TYPE_CHECKING:
from app.ai_provider.config.models import ModelConfig, ModelEndpointConfig
class AudioServiceError(AIProviderError):
"""ASR / TTS 服务失败的基础错误。"""
# ── ASR ───────────────────────────────────────────────────────────────────
class ASRRequest:
"""将音频转写为文本的请求。
属性:
audio_data: 原始音频字节。
format: 音频容器格式(如 ``"wav"``、``"mp3"``、``"m4a"``)。
language: 可选的 ISO-639-1 语言提示(如 ``"zh"``、``"en"``)。
"""
__slots__ = ("audio_data", "format", "language")
def __init__(
self,
audio_data: bytes,
format: str = "wav",
language: str | None = None,
) -> None:
self.audio_data = audio_data
self.format = format
self.language = language
@dataclass(frozen=True)
class ASRResponse:
"""成功转写的结果。"""
text: str
@runtime_checkable
class ASRProtocol(Protocol):
"""ASR 适配器的协议。"""
async def transcribe(self, request: ASRRequest) -> ASRResponse:
"""将音频字节转写为文本。
异常:
AudioServiceError: API 调用失败或响应异常时抛出。
"""
...
# ── TTS ───────────────────────────────────────────────────────────────────
class TTSRequest:
"""将文本合成为语音的请求。
属性:
text: 待朗读的文本。
voice: 音色标识符(模型决定可用音色)。
response_format: 期望的音频格式(``"wav"``、``"mp3"``、``"opus"``)。
extra: 厂商特定的额外参数(如音色克隆/设计参数)。
"""
__slots__ = ("text", "voice", "response_format", "extra")
def __init__(
self,
text: str,
voice: str = "default",
response_format: str = "wav",
extra: dict | None = None,
) -> None:
self.text = text
self.voice = voice
self.response_format = response_format
self.extra = extra or {}
@dataclass(frozen=True)
class TTSResponse:
"""成功合成的结果。"""
audio_data: bytes
format: str
@runtime_checkable
class TTSProtocol(Protocol):
"""TTS 适配器的协议。"""
async def synthesize(self, request: TTSRequest) -> TTSResponse:
"""将文本合成为音频字节。
异常:
AudioServiceError: API 调用失败或响应异常时抛出。
"""
...
# ── 共享工厂基类 ──────────────────────────────────────────────────────────
class AudioAdapterBase:
"""ASR/TTS 适配器的基类,提供共享的工厂方法。
子类必须设置 ``_adapter_type``(如 ``"asr"``、``"tts"``),并在
``__init__`` 中接受 ``base_url``、``api_key``、``model``、
``request_timeout_seconds`` 参数。
"""
_adapter_type: str = "audio"
_default_timeout: int = 120
@classmethod
def from_endpoint(
cls,
endpoint: ModelEndpointConfig,
*,
provider: ProviderConfig | None = None,
timeout_seconds: int | None = None,
) -> Any:
"""从已解析的端点配置构建适配器。
凭证解析优先级:
1. endpoint 直填的 ``base_url`` / ``api_key``
2. endpoint 的 ``base_url_env`` / ``api_key_env`` 指向的 settings 字段
3. provider 级凭证(调用方按需传入 ProviderConfig)
"""
base_url = _resolve_base_url(endpoint, provider)
api_key = _resolve_api_key(endpoint, provider)
if not base_url:
raise AudioServiceError(
status_code=500,
code=f"audio_{cls._adapter_type}_endpoint_invalid",
message=(
f"{cls._adapter_type.upper()} endpoint '{endpoint.name}' "
"has no resolved base_url."
),
)
return cls(
base_url=base_url,
api_key=api_key,
model=endpoint.model,
request_timeout_seconds=timeout_seconds
or endpoint.request_timeout_seconds
or cls._default_timeout,
)
@classmethod
def from_model_config(
cls,
config: ModelConfig,
*,
provider: ProviderConfig | None = None,
timeout_seconds: int | None = None,
) -> Any:
"""从逻辑模型配置构建适配器(使用第一个活跃端点)。"""
for ep in config.endpoints:
if ep.status == "active":
return cls.from_endpoint(
ep, provider=provider, timeout_seconds=timeout_seconds
)
raise AudioServiceError(
status_code=500,
code=f"audio_{cls._adapter_type}_no_endpoint",
message=f"{cls._adapter_type.upper()} model '{config.name}' has no active endpoint.",
)
def _resolve_base_url(
endpoint: ModelEndpointConfig,
provider: ProviderConfig | None,
) -> str:
"""端点 base_url → endpoint.base_url_env 对应 settings → provider.base_url(_env)。"""
if endpoint.base_url:
return endpoint.base_url
if endpoint.base_url_env:
val = _settings_get(endpoint.base_url_env)
if val:
return val
if provider is not None:
if provider.base_url:
return provider.base_url
if provider.base_url_env:
val = _settings_get(provider.base_url_env)
if val:
return val
return ""
def _resolve_api_key(
endpoint: ModelEndpointConfig,
provider: ProviderConfig | None,
) -> str:
"""端点 api_key → endpoint.api_key_env 对应 settings → provider 级凭证。"""
if endpoint.api_key:
return endpoint.api_key
if endpoint.api_key_env:
val = _settings_get(endpoint.api_key_env)
if val:
return val
if provider is not None:
# ProviderConfig 的 api_key_env / base_url_env 也走 settings 解析
val = _settings_get(provider.api_key_env)
if val:
return val
return ""
def _settings_get(env_name: str) -> str | None:
"""按大写 env 名查 settings 上的小写字段(与现有 _resolve_credentials 约定一致)。"""
from app.core.config import settings
key = env_name.lower()
if hasattr(settings, key):
value = getattr(settings, key)
return value if isinstance(value, str) else None
return None