From 462d570751903083808a656ee6b1869f98c619dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=AD=86?= <2685475410@qq.com> Date: Thu, 27 Aug 2026 10:02:44 +0800 Subject: [PATCH] =?UTF-8?q?M4b=EF=BC=9Aai=5Fprovider=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=89=8B=E6=9C=AF=E2=91=A0-=E2=91=A5=E2=80=94=E2=80=94?= =?UTF-8?q?=E7=A5=9E=E8=AF=86=E5=85=A8=E9=9D=A2=E6=8E=A5=E7=AE=A1=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=87=BA=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ① 后端删除 app/ai_provider/(38 文件,含 anthropic/openai 格式、billing、audio、embedding)、services/ai_provider_admin.py、core/config/model_config.py、tests/ai_provider/(21 文件) - ② 连带退役 application/agent/(旧 Agent 循环/steer/follow-up/确认注册表)、application/skill_executor.py、app/skills/(python skills 孤儿)、endpoints/audio.py、application/imports.py(ProviderManager 消费方);settings 端点收敛为生成偏好 - ③ runs.py 修剪:AgentRunRegistry/steer/follow-up/confirmations 端点退役,保留 list/get/events/cancel/stream;safety 确认改为直接落库裁决(pending→approved/denied) - ④ 前端删除:models 页、ModelGatewayView、gateway.ts、GatewayPanel(玄关)、ModelGatewayPanel/ModelStrategyForm、audio.ts/useVoiceInput、网关类型与 mock;设置页收敛为外观/语言/通知 - ⑤ 配置收敛:config/models|providers|pricing.example.yaml 归档 shenshi/config/;.env.example 删 provider keys/MODEL_ENDPOINT_POOL/MODEL_CONFIG_PATH,收敛 SHENSHI_* - ⑥ 验收:rg 审计灵台 Provider SDK import 与 ai_provider 引用归零;后端 528 passed(2 个预存失败经 HEAD 复跑验证与本手术无关)、前端 type-check+201 passed、神识 go vet+test、BFF 4 passed --- .env.example | 55 +- AGENTS.md | 2 +- backend/app/ai_provider/__init__.py | 63 -- backend/app/ai_provider/audio/__init__.py | 197 ---- backend/app/ai_provider/audio/asr.py | 263 ----- backend/app/ai_provider/audio/base.py | 245 ----- backend/app/ai_provider/audio/tts.py | 325 ------ backend/app/ai_provider/audio/yuanqi_asr.py | 231 ----- backend/app/ai_provider/config/__init__.py | 31 - backend/app/ai_provider/config/loader.py | 80 -- backend/app/ai_provider/config/models.py | 158 --- backend/app/ai_provider/core/__init__.py | 17 - backend/app/ai_provider/core/billing.py | 253 ----- backend/app/ai_provider/core/registry.py | 164 --- backend/app/ai_provider/core/resilience.py | 133 --- backend/app/ai_provider/embedding/__init__.py | 149 --- backend/app/ai_provider/embedding/base.py | 144 --- .../ai_provider/embedding/openai_format.py | 169 --- backend/app/ai_provider/llm/__init__.py | 18 - .../app/ai_provider/llm/anthropic_format.py | 433 -------- backend/app/ai_provider/llm/openai_format.py | 251 ----- backend/app/ai_provider/llm/pool.py | 164 --- backend/app/ai_provider/llm/stream.py | 115 --- backend/app/ai_provider/manager.py | 463 --------- backend/app/ai_provider/pipeline.py | 172 ---- backend/app/ai_provider/pipeline_handlers.py | 153 --- backend/app/ai_provider/reranker/__init__.py | 135 --- backend/app/ai_provider/reranker/base.py | 147 --- .../app/ai_provider/reranker/cohere_format.py | 186 ---- backend/app/ai_provider/routing/__init__.py | 14 - backend/app/ai_provider/routing/multimodal.py | 81 -- .../app/ai_provider/routing/permissions.py | 100 -- backend/app/ai_provider/routing/selector.py | 111 -- backend/app/ai_provider/types/__init__.py | 21 - backend/app/ai_provider/types/chat.py | 41 - backend/app/ai_provider/types/errors.py | 33 - backend/app/ai_provider/types/messages.py | 81 -- backend/app/api/router.py | 2 - backend/app/api/v1/deps.py | 8 - backend/app/api/v1/endpoints/audio.py | 275 ----- backend/app/api/v1/endpoints/runs.py | 113 +- backend/app/api/v1/endpoints/safety.py | 85 +- backend/app/api/v1/endpoints/settings.py | 41 +- backend/app/application/agent/__init__.py | 82 -- backend/app/application/agent/agent.py | 303 ------ backend/app/application/agent/confirmation.py | 148 --- backend/app/application/agent/extensions.py | 229 ----- .../app/application/agent/prompt_composer.py | 21 - .../app/application/agent/python_skills.py | 132 --- backend/app/application/agent/run_registry.py | 49 - backend/app/application/agent/runtime.py | 840 --------------- backend/app/application/agent/skill_loader.py | 22 - backend/app/application/agent/tasks.py | 95 -- backend/app/application/agent/tools.py | 137 --- backend/app/application/imports.py | 198 ---- backend/app/application/skill_executor.py | 83 -- backend/app/core/config/__init__.py | 3 - backend/app/core/config/model_config.py | 46 - backend/app/core/config/settings.py | 19 +- backend/app/main.py | 19 - backend/app/modules/character/__init__.py | 2 +- backend/app/modules/time_sense/__init__.py | 2 +- backend/app/schemas/preferences.py | 94 +- backend/app/schemas/runtime.py | 30 +- backend/app/services/ai_provider_admin.py | 108 -- backend/app/services/preferences.py | 93 +- backend/app/services/runtime.py | 19 - backend/app/skills/__init__.py | 5 - backend/app/skills/arg_guard.py | 39 - backend/app/skills/budget_guard.py | 34 - backend/app/skills/context_budget.py | 47 - backend/app/skills/response_guard.py | 44 - backend/app/skills/run_metrics.py | 53 - backend/tests/ai_provider/config/__init__.py | 0 .../tests/ai_provider/config/test_loader.py | 41 - .../tests/ai_provider/config/test_models.py | 69 -- backend/tests/ai_provider/core/__init__.py | 0 .../tests/ai_provider/core/test_billing.py | 67 -- .../tests/ai_provider/core/test_registry.py | 225 ---- .../tests/ai_provider/core/test_resilience.py | 109 -- .../ai_provider/llm/test_anthropic_format.py | 543 ---------- .../ai_provider/llm/test_openai_format.py | 114 --- backend/tests/ai_provider/llm/test_pool.py | 100 -- backend/tests/ai_provider/llm/test_stream.py | 168 --- .../ai_provider/routing/test_multimodal.py | 139 --- .../ai_provider/routing/test_permissions.py | 141 --- .../ai_provider/routing/test_selector.py | 135 --- backend/tests/ai_provider/test_manager.py | 61 -- backend/tests/ai_provider/test_pipeline.py | 91 -- .../ai_provider/test_pipeline_handlers.py | 305 ------ .../tests/ai_provider/types/test_messages.py | 46 - .../ai_provider/types/test_provider_errors.py | 21 - .../tests/api/test_model_gateway_endpoints.py | 78 -- .../agent/test_agent_foundation.py | 203 ---- .../application/agent/test_python_skills.py | 235 ----- .../application/agent/test_stateful_agent.py | 405 -------- .../tool_runtime/test_run_scoped_view.py | 101 -- backend/tests/conftest.py | 4 - backend/tests/test_agent_runtime.py | 965 ------------------ backend/tests/test_api.py | 1 - backend/tests/test_confirmation_endpoints.py | 134 --- docker/README.md | 2 +- frontend/next.config.ts | 15 +- frontend/src/app/(app)/models/page.tsx | 52 - frontend/src/app/(app)/settings/page.tsx | 35 +- .../src/app/admin/pivots/[pivot]/page.tsx | 13 - frontend/src/app/admin/tools/page.tsx | 18 +- .../src/components/admin/mcp/GatewayPanel.tsx | 743 -------------- frontend/src/components/layout/Sidebar.tsx | 2 - .../components/models/ModelGatewayView.tsx | 347 ------- .../components/settings/ModelGatewayPanel.tsx | 164 --- .../components/settings/ModelStrategyForm.tsx | 144 --- .../src/components/settings/SettingsView.tsx | 56 +- frontend/src/lib/api/audio.ts | 97 -- frontend/src/lib/api/gateway.ts | 135 --- frontend/src/lib/api/index.ts | 1 - frontend/src/lib/api/misc.test.ts | 19 - frontend/src/lib/api/settings.ts | 29 +- frontend/src/lib/constants/labels.ts | 44 - frontend/src/lib/hooks/useVoiceInput.test.ts | 117 --- frontend/src/lib/hooks/useVoiceInput.ts | 204 ---- frontend/src/lib/mock/data.ts | 114 --- frontend/src/lib/mock/index.ts | 54 - frontend/src/types/api.ts | 80 -- frontend/src/types/domain.ts | 35 - shenshi/config/README.md | 9 + .../config}/models.example.yaml | 0 .../config}/pricing.example.yaml | 0 .../config}/providers.example.yaml | 0 129 files changed, 81 insertions(+), 15867 deletions(-) delete mode 100644 backend/app/ai_provider/__init__.py delete mode 100644 backend/app/ai_provider/audio/__init__.py delete mode 100644 backend/app/ai_provider/audio/asr.py delete mode 100644 backend/app/ai_provider/audio/base.py delete mode 100644 backend/app/ai_provider/audio/tts.py delete mode 100644 backend/app/ai_provider/audio/yuanqi_asr.py delete mode 100644 backend/app/ai_provider/config/__init__.py delete mode 100644 backend/app/ai_provider/config/loader.py delete mode 100644 backend/app/ai_provider/config/models.py delete mode 100644 backend/app/ai_provider/core/__init__.py delete mode 100644 backend/app/ai_provider/core/billing.py delete mode 100644 backend/app/ai_provider/core/registry.py delete mode 100644 backend/app/ai_provider/core/resilience.py delete mode 100644 backend/app/ai_provider/embedding/__init__.py delete mode 100644 backend/app/ai_provider/embedding/base.py delete mode 100644 backend/app/ai_provider/embedding/openai_format.py delete mode 100644 backend/app/ai_provider/llm/__init__.py delete mode 100644 backend/app/ai_provider/llm/anthropic_format.py delete mode 100644 backend/app/ai_provider/llm/openai_format.py delete mode 100644 backend/app/ai_provider/llm/pool.py delete mode 100644 backend/app/ai_provider/llm/stream.py delete mode 100644 backend/app/ai_provider/manager.py delete mode 100644 backend/app/ai_provider/pipeline.py delete mode 100644 backend/app/ai_provider/pipeline_handlers.py delete mode 100644 backend/app/ai_provider/reranker/__init__.py delete mode 100644 backend/app/ai_provider/reranker/base.py delete mode 100644 backend/app/ai_provider/reranker/cohere_format.py delete mode 100644 backend/app/ai_provider/routing/__init__.py delete mode 100644 backend/app/ai_provider/routing/multimodal.py delete mode 100644 backend/app/ai_provider/routing/permissions.py delete mode 100644 backend/app/ai_provider/routing/selector.py delete mode 100644 backend/app/ai_provider/types/__init__.py delete mode 100644 backend/app/ai_provider/types/chat.py delete mode 100644 backend/app/ai_provider/types/errors.py delete mode 100644 backend/app/ai_provider/types/messages.py delete mode 100644 backend/app/api/v1/endpoints/audio.py delete mode 100644 backend/app/application/agent/__init__.py delete mode 100644 backend/app/application/agent/agent.py delete mode 100644 backend/app/application/agent/confirmation.py delete mode 100644 backend/app/application/agent/extensions.py delete mode 100644 backend/app/application/agent/prompt_composer.py delete mode 100644 backend/app/application/agent/python_skills.py delete mode 100644 backend/app/application/agent/run_registry.py delete mode 100644 backend/app/application/agent/runtime.py delete mode 100644 backend/app/application/agent/skill_loader.py delete mode 100644 backend/app/application/agent/tasks.py delete mode 100644 backend/app/application/agent/tools.py delete mode 100644 backend/app/application/imports.py delete mode 100644 backend/app/application/skill_executor.py delete mode 100644 backend/app/core/config/model_config.py delete mode 100644 backend/app/services/ai_provider_admin.py delete mode 100644 backend/app/skills/__init__.py delete mode 100644 backend/app/skills/arg_guard.py delete mode 100644 backend/app/skills/budget_guard.py delete mode 100644 backend/app/skills/context_budget.py delete mode 100644 backend/app/skills/response_guard.py delete mode 100644 backend/app/skills/run_metrics.py delete mode 100644 backend/tests/ai_provider/config/__init__.py delete mode 100644 backend/tests/ai_provider/config/test_loader.py delete mode 100644 backend/tests/ai_provider/config/test_models.py delete mode 100644 backend/tests/ai_provider/core/__init__.py delete mode 100644 backend/tests/ai_provider/core/test_billing.py delete mode 100644 backend/tests/ai_provider/core/test_registry.py delete mode 100644 backend/tests/ai_provider/core/test_resilience.py delete mode 100644 backend/tests/ai_provider/llm/test_anthropic_format.py delete mode 100644 backend/tests/ai_provider/llm/test_openai_format.py delete mode 100644 backend/tests/ai_provider/llm/test_pool.py delete mode 100644 backend/tests/ai_provider/llm/test_stream.py delete mode 100644 backend/tests/ai_provider/routing/test_multimodal.py delete mode 100644 backend/tests/ai_provider/routing/test_permissions.py delete mode 100644 backend/tests/ai_provider/routing/test_selector.py delete mode 100644 backend/tests/ai_provider/test_manager.py delete mode 100644 backend/tests/ai_provider/test_pipeline.py delete mode 100644 backend/tests/ai_provider/test_pipeline_handlers.py delete mode 100644 backend/tests/ai_provider/types/test_messages.py delete mode 100644 backend/tests/ai_provider/types/test_provider_errors.py delete mode 100644 backend/tests/api/test_model_gateway_endpoints.py delete mode 100644 backend/tests/application/agent/test_agent_foundation.py delete mode 100644 backend/tests/application/agent/test_python_skills.py delete mode 100644 backend/tests/application/agent/test_stateful_agent.py delete mode 100644 backend/tests/application/tool_runtime/test_run_scoped_view.py delete mode 100644 backend/tests/test_agent_runtime.py delete mode 100644 backend/tests/test_confirmation_endpoints.py delete mode 100644 frontend/src/app/(app)/models/page.tsx delete mode 100644 frontend/src/components/admin/mcp/GatewayPanel.tsx delete mode 100644 frontend/src/components/models/ModelGatewayView.tsx delete mode 100644 frontend/src/components/settings/ModelGatewayPanel.tsx delete mode 100644 frontend/src/components/settings/ModelStrategyForm.tsx delete mode 100644 frontend/src/lib/api/audio.ts delete mode 100644 frontend/src/lib/api/gateway.ts delete mode 100644 frontend/src/lib/hooks/useVoiceInput.test.ts delete mode 100644 frontend/src/lib/hooks/useVoiceInput.ts create mode 100644 shenshi/config/README.md rename {config => shenshi/config}/models.example.yaml (100%) rename {config => shenshi/config}/pricing.example.yaml (100%) rename {config => shenshi/config}/providers.example.yaml (100%) diff --git a/.env.example b/.env.example index 9769f7d..dc277de 100644 --- a/.env.example +++ b/.env.example @@ -88,7 +88,7 @@ LOG_ROTATION_SIZE=10 MB # 本地单用户范围标识,当前阶段不实现多用户、组织、租户、OAuth 或 SSO DEFAULT_OWNER_SCOPE=local-user -# 默认逻辑模型名。必须指向 config/models.yaml 中真实远程 provider 的逻辑模型 +# 默认逻辑模型名(提示值;实际路由由神识端点目录决定) DEFAULT_MODEL_HINT=deepseek-v4-flash # --- 管理端点和认证 --- @@ -207,7 +207,7 @@ BIG_CYCLE_MEMORY_THRESHOLD=20 # 两次大周天最小间隔(秒),避免频繁触发 BIG_CYCLE_MIN_INTERVAL_SECONDS=300 -# 大周天候选生成所用模型(config/models.yaml 的逻辑模型名) +# 大周天候选生成所用模型(逻辑模型名;路由由神识决定) BIG_CYCLE_MODEL=deepseek-v4-flash # 单次大周天生成的演化候选上限 @@ -238,7 +238,7 @@ MCP_STDIO_CALL_TIMEOUT_SECONDS=30 # MCP stdio SIGTERM → SIGKILL 等待秒数 MCP_STDIO_STOP_TIMEOUT_SECONDS=5 -# Embedding 模型逻辑名。指向 config/models.yaml 中已注册的 embedding 模型 +# Embedding 模型逻辑名(路由由神识决定) EMBEDDING_MODEL_HINT=bge-m3 # --- 感知与导入 --- @@ -259,53 +259,12 @@ IMPORT_ALLOWED_AUDIO_MIME_TYPES=audio/wav,audio/mpeg,audio/mp4,audio/flac # ===================================================================== -# 模型网关 · 神识(LLM Provider API Keys) +# 模型网关 · 神识(Go 网关统一接管,灵台不经手任何 Provider 密钥) # ===================================================================== -# 模型名称、provider 映射、能力标签、超时、限流、价格等,统一放在 config/models.yaml 中。 -# .env 只保存各 provider 的 URL 与 API key,避免模型策略散落在环境变量中。 +# 模型端点目录(含 base_url / api_key / 上游模型 / 优先级)统一经神识环境变量 SHENSHI_ENDPOINTS 配置, +# 详见下方「神识 · 模型网关」段;旧 config/models|providers|pricing.yaml 与灵台侧各 provider +# API Key 已随 ai_provider 退役(归档于 shenshi/config/ 仅供参考)。 -# --- DeepSeek --- -DEEPSEEK_API_KEY= -DEEPSEEK_BASE_URL=https://api.deepseek.com - -# --- 火山方舟 / 豆包 --- -VOLCENGINE_ARK_API_KEY= -VOLCENGINE_ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 - -# --- 小米 MiMo --- -XIAOMI_MIMO_API_KEY= -XIAOMI_MIMO_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1 - -# --- OpenAI --- -OPENAI_API_KEY= -OPENAI_BASE_URL=https://api.openai.com/v1 - -# --- Anthropic --- -ANTHROPIC_API_KEY= -ANTHROPIC_BASE_URL=https://api.anthropic.com - -# --- 元炁(本地推理引擎)--- -# 作为 OpenAI 兼容 provider 接入,供 bge-m3 embedding 等模型调用。 -YUANQI_BASE_URL=http://localhost:11434 -YUANQI_API_KEY= - -# --- 其他兼容网关预留 --- -CUSTOM_LLM_API_KEY= -CUSTOM_LLM_BASE_URL= - -# --- 模型 endpoint 池路由(高级调优项,保持默认即可正常工作)--- -# 物理 endpoint 池的 failover 与重试策略 -MODEL_ENDPOINT_POOL_ENABLED=true -MODEL_ENDPOINT_POOL_DRY_RUN_SELECTION=false -MODEL_ENDPOINT_POOL_EXPOSE_DEBUG_HEADERS=false -MODEL_ENDPOINT_POOL_AGENT_FAILOVER_ENABLED=false -MODEL_ENDPOINT_POOL_PER_ENDPOINT_MAX_RETRIES=1 -MODEL_ENDPOINT_POOL_RETRY_BASE_DELAY_SECONDS=0.25 -MODEL_ENDPOINT_POOL_RETRY_MAX_DELAY_SECONDS=2.0 - -# 模型配置文件路径 -# 默认读取项目根目录 config/models.yaml;不存在时 fallback 到 config/models.example.yaml -MODEL_CONFIG_PATH=../config/models.yaml # ===================================================================== diff --git a/AGENTS.md b/AGENTS.md index 62e2996..04fc1bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -244,7 +244,7 @@ make test # 全部测试(backend / shenshi / bff / fronten ```bash # 先准备配置(只需一次) cp .env.example .env # 填写 POSTGRES_PASSWORD 与各 *_API_KEY -cp config/models.example.yaml config/models.yaml # 按需填写真实模型 endpoint +# 模型端点目录经 SHENSHI_ENDPOINTS 配置(见 .env.example 神识段),无需复制 yaml # 1) 启动基础设施(首次创建 hunjing-net 网络;Milvus 会自动带起 hunjing-etcd / hunjing-minio) docker compose -f docker/docker-compose.infra.yml up -d diff --git a/backend/app/ai_provider/__init__.py b/backend/app/ai_provider/__init__.py deleted file mode 100644 index 7c38a32..0000000 --- a/backend/app/ai_provider/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -from app.ai_provider.config.loader import load_models_config, load_providers_config -from app.ai_provider.config.models import ModelConfig, ProviderConfig - -# 嵌入 / 重排序 -from app.ai_provider.embedding import ( - EmbeddingProtocol, - EmbeddingRequest, - EmbeddingResponse, - EmbeddingResult, - EmbeddingService, - EmbeddingServiceError, -) -from app.ai_provider.manager import ProviderManager, ProviderManagerFactory -from app.ai_provider.reranker import ( - RerankerProtocol, - RerankerService, - RerankerServiceError, - RerankRequest, - RerankResponse, - RerankResult, -) -from app.ai_provider.types.chat import ChatChunk, ChatResult, TokenUsage -from app.ai_provider.types.errors import AIProviderError -from app.ai_provider.types.messages import ( - AIMessage, - BaseMessage, - HumanMessage, - SystemMessage, - ToolMessage, -) - -__all__ = [ - # LLM - "ProviderManager", - "ProviderManagerFactory", - "BaseMessage", - "SystemMessage", - "HumanMessage", - "AIMessage", - "ToolMessage", - "ChatChunk", - "ChatResult", - "TokenUsage", - "AIProviderError", - "ModelConfig", - "ProviderConfig", - "load_models_config", - "load_providers_config", - # Embedding - "EmbeddingProtocol", - "EmbeddingRequest", - "EmbeddingResponse", - "EmbeddingResult", - "EmbeddingService", - "EmbeddingServiceError", - # Reranker - "RerankerProtocol", - "RerankRequest", - "RerankResponse", - "RerankResult", - "RerankerService", - "RerankerServiceError", -] diff --git a/backend/app/ai_provider/audio/__init__.py b/backend/app/ai_provider/audio/__init__.py deleted file mode 100644 index 1b3a934..0000000 --- a/backend/app/ai_provider/audio/__init__.py +++ /dev/null @@ -1,197 +0,0 @@ -"""音频服务 — ASR / TTS 适配器外观。 - -用法:: - - from app.ai_provider.audio import AudioService - - service = AudioService(factory) - result = await service.transcribe(audio_bytes, format="wav") - result = await service.synthesize("你好,魂镜", voice="default") - -``AudioService`` 通过注册表中带有 ``asr`` / ``tts`` 能力标签的模型来解析 -使用哪个模型,然后将创建委托给 ``ModelFactory.create_audio()``。 -""" - -from __future__ import annotations - -from app.ai_provider.audio.base import ( - ASRProtocol, - ASRRequest, - ASRResponse, - AudioServiceError, - TTSProtocol, - TTSRequest, - TTSResponse, -) - - -class AudioService: - """外观类,负责解析 ASR / TTS 模型并委托给 ProviderManager。 - - 接受 ProviderManager 或实现了以下接口的适配器: - - ``.registry`` → 具有 ``list_capability_models(capability: str)`` 方法 - - ``.create_audio(name: str)`` → 返回 ASRProtocol / TTSProtocol 实例 - """ - - def __init__(self, manager) -> None: - self._manager = manager - self._asr_adapter: ASRProtocol | None = None - self._tts_adapter: TTSProtocol | None = None - - # ── Registry 兼容适配 ───────────────────────────────────────── - - @property - def _registry(self): - """按需获取 registry,兼容 adapter 和裸 ProviderManager/ModelRegistry。""" - mgr = self._manager - if hasattr(mgr, "registry"): - return mgr.registry - if hasattr(mgr, "model_registry"): - return mgr.model_registry - return mgr - - def _list_capability_models(self, tag: str): - reg = self._registry - # 适配器接口(list_capability_models)vs ModelRegistry 接口(list_by_capability) - if hasattr(reg, "list_capability_models"): - return reg.list_capability_models(tag) - if hasattr(reg, "list_by_capability"): - return reg.list_by_capability(tag) - return [] - - @property - def _factory(self): - mgr = self._manager - if hasattr(mgr, "create_audio"): - return mgr - return None - - # ── ASR ────────────────────────────────────────────────────────────── - - @property - def asr(self) -> ASRProtocol: - """解析并缓存 ASR 适配器(取注册表中第一个活跃 ASR 模型)。""" - if self._asr_adapter is None: - self._asr_adapter = self._resolve_asr() - return self._asr_adapter - - def asr_for(self, model_name: str) -> ASRProtocol: - """解析并缓存指定模型名的 ASR 适配器(覆盖默认第一个)。""" - self._asr_adapter = self._factory.create_audio(model_name) - return self._asr_adapter - - async def transcribe( - self, - audio_data: bytes, - format: str = "wav", - language: str | None = None, - model_name: str | None = None, - ) -> ASRResponse: - """快捷方法:构造请求并调用 ASR 适配器。 - - Args: - model_name: 可选,指定使用哪个 ASR 模型(按注册表逻辑名)。 - 省略时取注册表中第一个活跃的 ASR 模型。 - """ - if model_name is not None: - adapter = self.asr_for(model_name) - else: - adapter = self.asr - request = ASRRequest( - audio_data=audio_data, - format=format, - language=language, - ) - return await adapter.transcribe(request) - - # ── TTS ────────────────────────────────────────────────────────────── - - @property - def tts(self) -> TTSProtocol: - """解析并缓存 TTS 适配器。""" - if self._tts_adapter is None: - self._tts_adapter = self._resolve_tts() - return self._tts_adapter - - async def synthesize( - self, - text: str, - voice: str = "default", - response_format: str = "wav", - extra: dict | None = None, - ) -> TTSResponse: - """快捷方法:构造请求并调用 TTS 适配器。""" - request = TTSRequest( - text=text, - voice=voice, - response_format=response_format, - extra=extra, - ) - return await self.tts.synthesize(request) - - # ── 解析 ─────────────────────────────────────────────────────── - - def _resolve_asr(self) -> ASRProtocol: - """查找第一个活跃的 ASR 模型并通过工厂创建。""" - models = self._list_capability_models("asr") - if not models: - raise AudioServiceError( - status_code=503, - code="audio_asr_no_model", - message="No ASR model configured in the model registry.", - ) - factory = self._factory - if factory is None: - raise AudioServiceError( - status_code=501, - code="audio_asr_no_factory", - message="Audio adapter factory not available (provider manager missing create_audio).", - ) - return factory.create_audio(models[0].name) - - def _resolve_tts(self) -> TTSProtocol: - """查找第一个活跃的 TTS 模型并通过工厂创建。""" - models = self._list_capability_models("tts") - if not models: - raise AudioServiceError( - status_code=503, - code="audio_tts_no_model", - message="No TTS model configured in the model registry.", - ) - factory = self._factory - if factory is None: - raise AudioServiceError( - status_code=501, - code="audio_tts_no_factory", - message="Audio adapter factory not available (provider manager missing create_audio).", - ) - return factory.create_audio(models[0].name) - - # ── 生命周期 ──────────────────────────────────────────────────────── - - async def close(self) -> None: - """释放适配器持有的 HTTP 连接。""" - for adapter in (self._asr_adapter, self._tts_adapter): - if adapter is None: - continue - close = getattr(adapter, "close", None) - if close is not None: - await close() - - async def __aenter__(self) -> AudioService: - return self - - async def __aexit__(self, *exc_info: object) -> None: - await self.close() - - -__all__ = [ - "ASRProtocol", - "ASRRequest", - "ASRResponse", - "AudioService", - "AudioServiceError", - "TTSProtocol", - "TTSRequest", - "TTSResponse", -] diff --git a/backend/app/ai_provider/audio/asr.py b/backend/app/ai_provider/audio/asr.py deleted file mode 100644 index 251a7b6..0000000 --- a/backend/app/ai_provider/audio/asr.py +++ /dev/null @@ -1,263 +0,0 @@ -"""MiMo ASR adapter — ``POST /v1/chat/completions`` with ``input_audio``. - -MiMo 的 ASR 不走独立音频端点,而是通过聊天补全接口扩展实现: - - POST {base_url}/chat/completions - Content-Type: application/json - Authorization: Bearer {api_key} - - { - "model": "mimo-v2.5-asr", - "messages": [ - { - "role": "user", - "content": [ - { - "type": "input_audio", - "input_audio": { - "data": "data:audio/wav;base64," - } - } - ] - } - ], - "asr_options": { - "language": "auto" - } - } - -响应:: - - { - "choices": [{ - "message": { - "content": "转写后的文本" - } - }] - } -""" - -from __future__ import annotations - -import base64 -import hashlib -import time - -import httpx - -from app.ai_provider.audio.base import ( - ASRProtocol, - ASRRequest, - ASRResponse, - AudioAdapterBase, - AudioServiceError, -) -from app.core.logging import logger - -_MIME_MAP = { - "wav": "audio/wav", - "mp3": "audio/mpeg", - "m4a": "audio/mp4", - "flac": "audio/flac", -} - -# ASR 缓存:默认最多 128 条,TTL 1 小时 -_ASR_CACHE_MAX_SIZE = 128 -_ASR_CACHE_TTL_SECONDS = 3600 - - -class _ASRCacheEntry: - __slots__ = ("text", "stored_at") - - def __init__(self, text: str) -> None: - self.text = text - self.stored_at = time.monotonic() - - -_asr_cache: dict[str, _ASRCacheEntry] = {} - - -def _asr_cache_key(model: str, audio_data: bytes, language: str, fmt: str) -> str: - """生成 ASR 缓存键:SHA256(model + audio_bytes + language + format)。""" - raw = f"{model}\0{language}\0{fmt}\0" - raw_bytes = raw.encode() + audio_data - return hashlib.sha256(raw_bytes).hexdigest() - - -def _asr_cache_get(key: str) -> str | None: - entry = _asr_cache.get(key) - if entry is None: - return None - if time.monotonic() - entry.stored_at > _ASR_CACHE_TTL_SECONDS: - del _asr_cache[key] - return None - return entry.text - - -def _asr_cache_set(key: str, text: str) -> None: - if len(_asr_cache) >= _ASR_CACHE_MAX_SIZE: - sorted_keys = sorted(_asr_cache.keys(), key=lambda k: _asr_cache[k].stored_at) - for old_key in sorted_keys[: _ASR_CACHE_MAX_SIZE // 2]: - del _asr_cache[old_key] - _asr_cache[key] = _ASRCacheEntry(text) - - -class HunjingASROpenAI(AudioAdapterBase, ASRProtocol): - """MiMo ASR 适配器,通过 ``POST /chat/completions`` 携带 ``input_audio`` 实现。 - - 将音频字节包装为 chat 消息中的 ``input_audio`` 内容类型的 data URL, - 然后从响应中提取转写文本。 - """ - - _adapter_type = "asr" - _default_timeout = 300 - - def __init__( - self, - *, - base_url: str, - api_key: str, - model: str = "mimo-v2.5-asr", - request_timeout_seconds: int = 300, - ) -> None: - self._base_url = base_url.rstrip("/") - self._api_key = api_key - self._model = model - self._timeout = request_timeout_seconds - self._client: httpx.AsyncClient | None = None - - # ── 客户端 ─────────────────────────────────────────────────────────── - - @property - def _http(self) -> httpx.AsyncClient: - if self._client is None or self._client.is_closed: - self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout)) - return self._client - - async def close(self) -> None: - if self._client is not None and not self._client.is_closed: - await self._client.aclose() - - # ── ASRProtocol ────────────────────────────────────────────────────── - - async def transcribe(self, request: ASRRequest) -> ASRResponse: - """通过 ``POST /chat/completions`` 转写音频。 - - 将音频编码为 data URL 并以 ``input_audio`` 内容块的形式发送到 chat 消息中。 - """ - # ── 缓存查找 ────────────────────────────────────────────────── - cache_key = _asr_cache_key( - self._model, request.audio_data, request.language or "auto", request.format - ) - cached = _asr_cache_get(cache_key) - if cached is not None: - logger.info("audio_asr.cache_hit", model=self._model, text_length=len(cached)) - return ASRResponse(text=cached) - - # ── 构造请求 ────────────────────────────────────────────────── - mime = _MIME_MAP.get(request.format, "audio/wav") - b64_audio = base64.b64encode(request.audio_data).decode() - data_url = f"data:{mime};base64,{b64_audio}" - - payload: dict = { - "model": self._model, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "input_audio", - "input_audio": { - "data": data_url, - }, - }, - ], - }, - ], - "asr_options": { - "language": request.language or "auto", - }, - } - - headers = { - "Authorization": f"Bearer {self._api_key}", - "Content-Type": "application/json", - } - - url = f"{self._base_url}/chat/completions" - - logger.info( - "audio_asr.request", - model=self._model, - format=request.format, - language=request.language, - url=url, - size_bytes=len(request.audio_data), - ) - - # ── API 调用 ──────────────────────────────────────────────── - try: - response = await self._http.post(url, json=payload, headers=headers) - except httpx.TimeoutException as exc: - raise AudioServiceError( - status_code=504, - code="audio_asr_timeout", - message="ASR request timed out calling chat completions.", - ) from exc - except httpx.RequestError as exc: - raise AudioServiceError( - status_code=503, - code="audio_asr_connection_error", - message=f"ASR connection error: {exc}", - ) from exc - - if response.status_code != 200: - body = response.text[:500] - raise AudioServiceError( - status_code=response.status_code, - code="audio_asr_api_error", - message=f"ASR API returned {response.status_code}: {body}", - ) - - # 解析响应,提取转写文本 - text = self._extract_text(response.json()) - if not text: - raise AudioServiceError( - status_code=500, - code="audio_asr_empty_response", - message="ASR API returned empty transcription.", - ) - - # ── 写入缓存 ──────────────────────────────────────────────── - _asr_cache_set(cache_key, text) - - logger.info( - "audio_asr.success", - model=self._model, - text_length=len(text), - cache_size=len(_asr_cache), - ) - return ASRResponse(text=text) - - # ── 辅助方法 ────────────────────────────────────────────────────────── - - @staticmethod - def _extract_text(body: dict) -> str: - """从 chat completion 响应中提取转写文本。""" - try: - choices = body.get("choices", []) - if not choices: - raise AudioServiceError( - status_code=500, - code="audio_asr_no_choices", - message="ASR response has no choices.", - ) - return (choices[0].get("message") or {}).get("content") or "" - except AudioServiceError: - raise - except Exception as exc: - raise AudioServiceError( - status_code=500, - code="audio_asr_response_parse_error", - message=f"Failed to parse ASR response: {exc}", - ) from exc diff --git a/backend/app/ai_provider/audio/base.py b/backend/app/ai_provider/audio/base.py deleted file mode 100644 index 28de0e7..0000000 --- a/backend/app/ai_provider/audio/base.py +++ /dev/null @@ -1,245 +0,0 @@ -"""音频服务类型 — 请求/响应模型和协议定义。 - -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 diff --git a/backend/app/ai_provider/audio/tts.py b/backend/app/ai_provider/audio/tts.py deleted file mode 100644 index ac0bb90..0000000 --- a/backend/app/ai_provider/audio/tts.py +++ /dev/null @@ -1,325 +0,0 @@ -"""MiMo TTS adapter — ``POST /v1/chat/completions`` with ``audio`` parameter. - -MiMo 的 TTS 不走独立音频端点,而是通过聊天补全接口扩展实现: - - POST {base_url}/chat/completions - Content-Type: application/json - Authorization: Bearer {api_key} - - { - "model": "mimo-v2.5-tts", - "messages": [ - {"role": "user", "content": "(可选)音色风格描述,voicedesign 必填"}, - {"role": "assistant", "content": "要朗读的文本"} - ], - "audio": { - "format": "wav", - "voice": "mimo_default" - } - } - -响应:: - - { - "choices": [{ - "message": { - "audio": { - "data": "", - "id": "...", - "expires_at": null, - "transcript": null - } - } - }] - } - -模型差异: - -- ``mimo-v2.5-tts`` — ``voice`` 可选,默认 ``mimo_default``,仅支持预置音色 -- ``mimo-v2.5-tts-voiceclone`` — ``voice`` 必填,传入音频样本的 base64(mp3/wav) -- ``mimo-v2.5-tts-voicedesign`` — 不支持 ``voice``,\n ``user`` 消息必填(风格描述),支持 ``optimize_text_preview`` -""" - -from __future__ import annotations - -import base64 -import hashlib -import time - -import httpx - -from app.ai_provider.audio.base import ( - AudioAdapterBase, - AudioServiceError, - TTSProtocol, - TTSRequest, - TTSResponse, -) -from app.core.logging import logger - -# voicedesign 模型的默认风格描述(用户未提供时使用) -_DEFAULT_VOICE_DESIGN_PROMPT = "自然、清晰的朗读风格,语气平和,语速适中,适合日常对话场景。" - -# TTS 音频缓存:默认最多 256 条,TTL 1 小时 -_TTS_CACHE_MAX_SIZE = 256 -_TTS_CACHE_TTL_SECONDS = 3600 - - -class _TTSCacheEntry: - __slots__ = ("data", "format", "stored_at") - - def __init__(self, data: bytes, format: str) -> None: - self.data = data - self.format = format - self.stored_at = time.monotonic() - - -_tts_cache: dict[str, _TTSCacheEntry] = {} - - -def _tts_cache_key(model: str, voice: str, fmt: str, text: str, extra_json: str) -> str: - """生成 TTS 缓存键:SHA256(model + voice + format + text + extra)。""" - raw = f"{model}\0{voice}\0{fmt}\0{text}\0{extra_json}" - return hashlib.sha256(raw.encode()).hexdigest() - - -def _tts_cache_get(key: str) -> _TTSCacheEntry | None: - entry = _tts_cache.get(key) - if entry is None: - return None - if time.monotonic() - entry.stored_at > _TTS_CACHE_TTL_SECONDS: - del _tts_cache[key] - return None - return entry - - -def _tts_cache_set(key: str, entry: _TTSCacheEntry) -> None: - if len(_tts_cache) >= _TTS_CACHE_MAX_SIZE: - # 淘汰最旧的一半 - sorted_keys = sorted(_tts_cache.keys(), key=lambda k: _tts_cache[k].stored_at) - for old_key in sorted_keys[: _TTS_CACHE_MAX_SIZE // 2]: - del _tts_cache[old_key] - _tts_cache[key] = entry - - -class HunjingTTSOpenAI(AudioAdapterBase, TTSProtocol): - """MiMo TTS 适配器,通过 ``POST /chat/completions`` 携带 ``audio`` 参数实现。 - - 调用兼容 OpenAI 格式的 chat completions 端点,在请求体中添加 ``audio`` 字段, - 然后从响应中提取并解码 base64 音频数据。 - """ - - _adapter_type = "tts" - _default_timeout = 120 - - def __init__( - self, - *, - base_url: str, - api_key: str, - model: str = "mimo-v2.5-tts", - request_timeout_seconds: int = 120, - ) -> None: - self._base_url = base_url.rstrip("/") - self._api_key = api_key - self._model = model - self._timeout = request_timeout_seconds - self._client: httpx.AsyncClient | None = None - - # ── 客户端 ─────────────────────────────────────────────────────────── - - @property - def _http(self) -> httpx.AsyncClient: - if self._client is None or self._client.is_closed: - self._client = httpx.AsyncClient(timeout=httpx.Timeout(self._timeout)) - return self._client - - async def close(self) -> None: - if self._client is not None and not self._client.is_closed: - await self._client.aclose() - - # ── TTSProtocol ────────────────────────────────────────────────────── - - async def synthesize(self, request: TTSRequest) -> TTSResponse: - """通过 ``POST /chat/completions`` 将文本合成为音频。 - - 构造 chat 消息和 ``audio`` 参数,匹配 MiMo 兼容 OpenAI 格式的 TTS, - 然后从响应中解码 base64 音频。 - - 结果按 ``(model, voice, format, text)`` 在进程内缓存,避免重复 API 调用。 - """ - # ── 缓存查找 ────────────────────────────────────────────────────── - cache_key = _tts_cache_key( - model=self._model, - voice=request.voice, - fmt=request.response_format, - text=request.text, - extra_json=str(sorted((request.extra or {}).items())), - ) - cached = _tts_cache_get(cache_key) - if cached is not None: - logger.info("audio_tts.cache_hit", model=self._model, text_length=len(request.text)) - return TTSResponse(audio_data=cached.data, format=cached.format) - - # ── API 调用 ────────────────────────────────────────────────────── - url = f"{self._base_url}/chat/completions" - - # 构造 messages - messages = self._build_messages(request) - - # 构造 audio 参数 - audio_params = self._build_audio_params(request) - - payload: dict = { - "model": self._model, - "messages": messages, - "audio": audio_params, - "max_completion_tokens": 8192, - } - # 合并 extra(如 frequency_penalty, temperature 等) - if request.extra: - payload.update(request.extra) - - headers = { - "Authorization": f"Bearer {self._api_key}", - "Content-Type": "application/json", - } - - logger.info( - "audio_tts.request", - model=self._model, - voice=request.voice, - audio_format=request.response_format, - text_length=len(request.text), - url=url, - ) - - try: - response = await self._http.post(url, json=payload, headers=headers) - except httpx.TimeoutException as exc: - raise AudioServiceError( - status_code=504, - code="audio_tts_timeout", - message="TTS request timed out calling chat completions.", - ) from exc - except httpx.RequestError as exc: - raise AudioServiceError( - status_code=503, - code="audio_tts_connection_error", - message=f"TTS connection error: {exc}", - ) from exc - - if response.status_code != 200: - body = response.text[:500] - raise AudioServiceError( - status_code=response.status_code, - code="audio_tts_api_error", - message=f"TTS API returned {response.status_code}: {body}", - ) - - # 解析响应,提取 base64 音频 - audio_data = self._extract_audio(response.json()) - - # ── 写入缓存 ────────────────────────────────────────────────────── - _tts_cache_set(cache_key, _TTSCacheEntry(audio_data, request.response_format)) - - logger.info( - "audio_tts.success", - model=self._model, - audio_bytes=len(audio_data), - cache_size=len(_tts_cache), - ) - return TTSResponse(audio_data=audio_data, format=request.response_format) - - # ── 辅助方法 ────────────────────────────────────────────────────────── - - def _build_messages(self, request: TTSRequest) -> list[dict]: - """构造 chat messages 数组。 - - - voicedesign 模型:user 消息必填(风格描述) - - 其他模型:user 消息可选,assistant 消息存放待朗读文本 - """ - is_voicedesign = "voicedesign" in self._model - - if is_voicedesign: - # voicedesign:user 消息为风格提示,assistant 消息为朗读文本 - style_prompt = (request.extra or {}).pop("style_prompt", _DEFAULT_VOICE_DESIGN_PROMPT) - return [ - {"role": "user", "content": style_prompt}, - {"role": "assistant", "content": request.text}, - ] - # 基础 TTS / voiceclone:只传 assistant 消息 - return [ - {"role": "assistant", "content": request.text}, - ] - - def _build_audio_params(self, request: TTSRequest) -> dict: - """构造 ``audio`` 参数字段,按模型处理 voice/format/optimize_text_preview 等。""" - params: dict = { - "format": _resolve_api_format(request.response_format), - } - - is_voicedesign = "voicedesign" in self._model - - if is_voicedesign: - # voicedesign 不支持 voice 字段 - optimize = (request.extra or {}).pop("optimize_text_preview", None) - if optimize is not None: - params["optimize_text_preview"] = optimize - else: - # 基础 TTS / voiceclone:按需传 voice - if request.voice and request.voice != "default": - params["voice"] = request.voice - else: - params["voice"] = "mimo_default" - - return params - - @staticmethod - def _extract_audio(body: dict) -> bytes: - """从 chat completion 响应中提取并解码 base64 音频数据。""" - try: - choices = body.get("choices", []) - if not choices: - raise AudioServiceError( - status_code=500, - code="audio_tts_no_choices", - message="TTS response has no choices.", - ) - message = choices[0].get("message", {}) - audio_info = message.get("audio") - if not audio_info: - raise AudioServiceError( - status_code=500, - code="audio_tts_no_audio_field", - message="TTS response missing audio field in message.", - ) - raw = audio_info.get("data") - if not raw: - raise AudioServiceError( - status_code=500, - code="audio_tts_empty_audio_data", - message="TTS response has empty audio.data.", - ) - return base64.b64decode(raw) - except AudioServiceError: - raise - except Exception as exc: - raise AudioServiceError( - status_code=500, - code="audio_tts_response_parse_error", - message=f"Failed to parse TTS response: {exc}", - ) from exc - - -def _resolve_api_format(user_format: str) -> str: - """将用户层面的格式名映射为 API 期望的格式。 - - MiMo TTS API 接受:``wav``、``mp3``、``pcm``、``pcm16``。 - """ - fmt = user_format.lower() - if fmt in ("pcm", "pcm16"): - return "pcm16" - if fmt in ("wav", "mp3"): - return fmt - return "wav" diff --git a/backend/app/ai_provider/audio/yuanqi_asr.py b/backend/app/ai_provider/audio/yuanqi_asr.py deleted file mode 100644 index 4c19720..0000000 --- a/backend/app/ai_provider/audio/yuanqi_asr.py +++ /dev/null @@ -1,231 +0,0 @@ -"""yuanqi 本地 ASR 适配器 — ``POST /v1/audio/transcriptions`` (multipart)。 - -与 MiMo 走 ``/chat/completions`` + ``input_audio`` 不同,元炁本地推理引擎 -(yuanqi)对外暴露的是 OpenAI Whisper 兼容的 multipart 端点:: - - POST {base_url}/v1/audio/transcriptions - Content-Type: multipart/form-data - Authorization: Bearer {api_key} - - file: