feat: 预置音色 preview_url 动态生成,解决签名 URL 过期问题 #1222

Merged
xiaoxia merged 1 commits from feat/preset-voice-dynamic-preview into develop 2026-08-02 22:14:12 +08:00
Owner

问题

预置音色的 8 个 preview_url 是 DashScope 签名 URL,有效期约 30 天,过期后前端无法播放试听。

方案

采用方案 A(推荐):修改 /voices/presets/voices 接口,返回时动态生成 preview_url,不再依赖硬编码的过期 URL。

改动

  • 新增 _resolve_preset_preview_url():缓存优先(7天TTL),缓存失效时调 CosyVoice TTS 重新合成,合成失败降级到硬编码 URL
  • 新增 _resolve_all_preset_preview_urls()ThreadPoolExecutor(max_workers=4) 并行生成 8 个音色的 preview_url,避免串行超时
  • 修改 list_preset_voices()list_voices_unified():注入 CosyVoiceService 依赖,动态解析 preview_url
  • 修改 _preset_to_unified_response():支持 preview_url_map 参数
  • 新增 13 个单元测试:覆盖缓存命中/失效/合成失败/并行解析/降级等场景

验证

  • 117 个相关测试全部通过
  • ruff + black 检查通过

关联

  • 解决预置音色 preview_url 过期问题
  • 关联 #1195(预置音色试听播放)
## 问题 预置音色的 8 个 `preview_url` 是 DashScope 签名 URL,有效期约 30 天,过期后前端无法播放试听。 ## 方案 采用**方案 A(推荐)**:修改 `/voices/presets` 和 `/voices` 接口,返回时动态生成 preview_url,不再依赖硬编码的过期 URL。 ## 改动 - **新增 `_resolve_preset_preview_url()`**:缓存优先(7天TTL),缓存失效时调 CosyVoice TTS 重新合成,合成失败降级到硬编码 URL - **新增 `_resolve_all_preset_preview_urls()`**:`ThreadPoolExecutor(max_workers=4)` 并行生成 8 个音色的 preview_url,避免串行超时 - **修改 `list_preset_voices()` 和 `list_voices_unified()`**:注入 `CosyVoiceService` 依赖,动态解析 preview_url - **修改 `_preset_to_unified_response()`**:支持 `preview_url_map` 参数 - **新增 13 个单元测试**:覆盖缓存命中/失效/合成失败/并行解析/降级等场景 ## 验证 - 117 个相关测试全部通过 - ruff + black 检查通过 ## 关联 - 解决预置音色 preview_url 过期问题 - 关联 #1195(预置音色试听播放)

🚀 预览环境已部署

项目 详情
PR号 #1222
预览链接 https://pr-1222.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1222 | | 预览链接 | [https://pr-1222.preview.xiaoxiajianji.com](https://pr-1222.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
xiaoxia added 1 commit 2026-08-02 22:08:12 +08:00
feat: 预置音色 preview_url 动态生成,解决签名 URL 过期问题
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 37s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m39s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m15s
AI Code Review / AI Code Review (pull_request) Successful in 4m12s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m26s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m0s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m22s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 40s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 41s
4e36aebabb
- 新增 _resolve_preset_preview_url():缓存优先,失效时调 CosyVoice TTS 重新合成,失败降级到硬编码 URL
- 新增 _resolve_all_preset_preview_urls():ThreadPoolExecutor(max_workers=4) 并行生成 8 个音色的 preview_url
- 修改 list_preset_voices() 和 list_voices_unified():注入 CosyVoiceService,动态解析 preview_url
- 修改 _preset_to_unified_response():支持 preview_url_map 参数
- 新增 13 个单元测试覆盖缓存命中/失效/合成失败/并行解析等场景

117 个相关测试全部通过,ruff + black 检查通过
xiaoxia force-pushed feat/preset-voice-dynamic-preview from 612ce94720 to 4e36aebabb 2026-08-02 22:08:12 +08:00 Compare
Collaborator

代码审查结果 - PR #1222

⚠️ 问题(3个需要修改)

  1. apps/api/app/api/routes/voices.py 第93行:异常处理逻辑错误,导致降级失效。

    • 原因:在 _resolve_all_preset_preview_urlsexcept Exception 块中,将结果设为空字符串 ""
    • 后果:如果 _resolve_preset_preview_url 内部发生未预料到的异常抛出(尽管它有 try-except,但仍存在抛出异常的可能,如参数传递错误),result_map 会存入空字符串。在 list_preset_voices 中,preview_url_map.get(p.voice_id, p.preview_url) 会因为 key 存在且值为 "" 而返回 "",导致前端拿到无效 URL,无法播放音频,且无法降级到硬编码的 fallback_url
    • 建议:修改 future_to_voice 的结构,使其能关联到 preset 对象或 fallback_url,以便在异常处理时能回退到 fallback_url,而不是空字符串。
  2. apps/api/app/api/routes/voices.py 第272行:在请求主线程中执行耗时任务,存在严重的性能隐患。

    • 原因list_voices_unified 接口负责返回音色列表,属于高频且对延迟敏感的路径。该接口内部调用了 _resolve_all_preset_preview_urls,虽然使用了线程池,但 as_completed 会阻塞等待所有 TTS 任务完成。
    • 后果:如果 CosyVoice 服务响应变慢或超时,会导致 list_voices_unified 接口响应时间显著增加,甚至导致请求超时,严重影响前端加载音色列表的用户体验。此外,该操作触发了“惊群效应”,多个并发请求会同时触发 TTS 合成。
    • 建议:建议将预览 URL 的生成改为异步后台任务(如 Celery)或采用“懒加载”策略(前端按需请求预览),避免阻塞列表查询接口。若必须在此处生成,应确保有较短的 timeout 机制,且仅对缓存失效的少数音色进行更新。
  3. apps/api/app/api/routes/voices.py 第58行:全局变量 _preset_preview_cache 存在线程安全隐患。

    • 原因_preset_preview_cache 是模块级别的全局字典,被 ThreadPoolExecutor 中的多个线程并发读写(检查存在性、读取、写入)。
    • 后果:虽然 CPython 的 GIL 对单个字节码指令有原子性保护,但“检查-然后-读取”或“检查-然后-写入”的操作不是原子的。这可能导致“惊群效应”(多个线程同时发现缓存未命中并重复调用 TTS),浪费大量计算资源和 API 配额。在极端情况下还可能引发字典运行时错误。
    • 建议:使用 threading.Lock 对缓存访问进行加锁保护,或者使用线程安全的数据结构。

💡 建议(1个可选)

  1. apps/api/app/api/routes/voices.py 第93行:建议补充日志记录。
    • 内容:在捕获 future.result() 的异常块中,除了回退处理外,建议添加 logger.error 记录具体的异常堆栈,以便排查为何 _resolve_preset_preview_url 抛出了未被内部捕获的异常。

格式检查通过 | 逻辑审查需修改 | ⚠️ 建议关注性能


🤖 由 AI 代码审查机器人自动生成 | 2026-08-02 14:12:26 | 模型:

## 代码审查结果 - PR #1222 ### ⚠️ 问题(3个需要修改) 1. **apps/api/app/api/routes/voices.py 第93行**:异常处理逻辑错误,导致降级失效。 - **原因**:在 `_resolve_all_preset_preview_urls` 的 `except Exception` 块中,将结果设为空字符串 `""`。 - **后果**:如果 `_resolve_preset_preview_url` 内部发生未预料到的异常抛出(尽管它有 try-except,但仍存在抛出异常的可能,如参数传递错误),`result_map` 会存入空字符串。在 `list_preset_voices` 中,`preview_url_map.get(p.voice_id, p.preview_url)` 会因为 key 存在且值为 `""` 而返回 `""`,导致前端拿到无效 URL,无法播放音频,且无法降级到硬编码的 `fallback_url`。 - **建议**:修改 `future_to_voice` 的结构,使其能关联到 `preset` 对象或 `fallback_url`,以便在异常处理时能回退到 `fallback_url`,而不是空字符串。 2. **apps/api/app/api/routes/voices.py 第272行**:在请求主线程中执行耗时任务,存在严重的性能隐患。 - **原因**:`list_voices_unified` 接口负责返回音色列表,属于高频且对延迟敏感的路径。该接口内部调用了 `_resolve_all_preset_preview_urls`,虽然使用了线程池,但 `as_completed` 会阻塞等待所有 TTS 任务完成。 - **后果**:如果 CosyVoice 服务响应变慢或超时,会导致 `list_voices_unified` 接口响应时间显著增加,甚至导致请求超时,严重影响前端加载音色列表的用户体验。此外,该操作触发了“惊群效应”,多个并发请求会同时触发 TTS 合成。 - **建议**:建议将预览 URL 的生成改为异步后台任务(如 Celery)或采用“懒加载”策略(前端按需请求预览),避免阻塞列表查询接口。若必须在此处生成,应确保有较短的 timeout 机制,且仅对缓存失效的少数音色进行更新。 3. **apps/api/app/api/routes/voices.py 第58行**:全局变量 `_preset_preview_cache` 存在线程安全隐患。 - **原因**:`_preset_preview_cache` 是模块级别的全局字典,被 `ThreadPoolExecutor` 中的多个线程并发读写(检查存在性、读取、写入)。 - **后果**:虽然 CPython 的 GIL 对单个字节码指令有原子性保护,但“检查-然后-读取”或“检查-然后-写入”的操作不是原子的。这可能导致“惊群效应”(多个线程同时发现缓存未命中并重复调用 TTS),浪费大量计算资源和 API 配额。在极端情况下还可能引发字典运行时错误。 - **建议**:使用 `threading.Lock` 对缓存访问进行加锁保护,或者使用线程安全的数据结构。 ### 💡 建议(1个可选) 1. **apps/api/app/api/routes/voices.py 第93行**:建议补充日志记录。 - **内容**:在捕获 `future.result()` 的异常块中,除了回退处理外,建议添加 `logger.error` 记录具体的异常堆栈,以便排查为何 `_resolve_preset_preview_url` 抛出了未被内部捕获的异常。 --- ✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-08-02 14:12:26 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
xiaoxia merged commit 7ef0366de0 into develop 2026-08-02 22:14:12 +08:00
xiaoxia deleted branch feat/preset-voice-dynamic-preview 2026-08-02 22:14:12 +08:00

🗑️ 预览环境已清理

PR #1222 已关闭或合并,对应的预览环境已被清理。

如有需要,可以重新打开 PR 来重新生成预览环境。

🗑️ **预览环境已清理** PR #1222 已关闭或合并,对应的预览环境已被清理。 > 如有需要,可以重新打开 PR 来重新生成预览环境。
Sign in to join this conversation.