# Technical Design: Voice Clone Provider Refactoring ## Context 当前语音克隆功能直接依赖阿里云 CosyVoice 的 SDK 和 API。Service 层直接调用 `CosyVoiceClient`,导致: 1. **强耦合**:无法轻松切换或添加其他供应商 2. **测试困难**:难以 mock 外部依赖 3. **扩展性差**:添加新供应商需要修改 Service 层 ## Goals / Non-Goals ### Goals - 解耦 Service 层与具体供应商实现 - 支持多供应商并存和动态切换 - 保持现有功能完全兼容 - 为添加硅基流动 IndexTTS-2 打下基础 ### Non-Goals - 不改变现有 API 行为 - 不修改数据库结构 - 不改变前端交互 ## Decisions ### 1. 采用策略模式 + 工厂模式 **Why**: - 策略模式:定义统一接口,各供应商独立实现 - 工厂模式:根据配置动态获取 Provider 实例 - 符合开闭原则,扩展时无需修改现有代码 **架构**: ``` VoiceCloneProvider (interface) ├── CosyVoiceProvider (impl) - 阿里云 CosyVoice (DashScope) ├── SiliconFlowProvider (impl) - 阶段二:硅基流动 IndexTTS-2 └── VoiceCloneProviderFactory ``` **说明**: - `CosyVoiceProvider` 对应阿里云 DashScope 的语音服务 - 默认模型:`cosyvoice-v3-flash` - 扩展时添加新的 Provider 实现 ### 2. 统一 DTO 设计 **Why**: 屏蔽不同供应商的 API 差异 ```java // 统一请求 VoiceCloneRequest { String audioUrl; // 音频 URL String prefix; // 音色前缀 String targetModel; // 目标模型 } // 统一响应 VoiceCloneResult { String voiceId; // 生成的音色 ID String requestId; // 请求 ID } ``` ### 3. 配置结构设计 **新配置结构**: ```yaml yudao: voice: # 默认供应商 default-provider: cosyvoice # 供应商配置 providers: cosyvoice: # 阿里云 CosyVoice enabled: true api-key: ${DASHSCOPE_API_KEY} default-model: cosyvoice-v3-flash # ... 其他配置 siliconflow: # 阶段二添加 enabled: false api-key: ${SILICONFLOW_API_KEY} base-url: https://api.siliconflow.cn default-model: indextts-2 ``` **向后兼容**: - 读取旧配置 `yudao.cosyvoice.*` 并合并到新结构 - 优先使用新配置,旧配置作为 fallback ### 4. 错误处理策略 - Provider 调用失败时,记录详细日志 - 返回统一的业务异常 `VOICE_TTS_FAILED` - 不暴露底层供应商的技术细节 ## Risks / Trade-offs | Risk | Mitigation | |------|------------| | 破坏现有功能 | 充分测试,保持 DTO 兼容 | | 配置迁移复杂 | 支持旧配置自动映射 | | 性能开销 | 工厂缓存 Provider 实例 | ## Migration Plan ### 阶段一:CosyVoice 重构 1. 创建接口和工厂 2. 重构 CosyVoice 为 Provider 实现 3. 更新 Service 层使用接口 4. 测试验证 ### 阶段二:添加 SiliconFlow 1. 实现 SiliconFlowProvider 2. 添加配置支持 3. 集成测试 ### 回滚方案 - 保留原有配置支持 - Feature Flag 控制新逻辑 ## Open Questions 1. **Q**: 是否需要支持运行时动态切换供应商? **A**: 初期不支持,通过配置切换即可 2. **Q**: 是否需要 Provider 健康检查? **A**: 阶段二考虑添加 3. **Q**: DTO 字段差异如何处理? **A**: 使用公共字段,扩展字段放 `Map extensions`