初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user