feat(voice): 提取视频配音接口 + 素材来源标识 #1654

Merged
xiaoxia merged 2 commits from feat/extract-video-voice-and-source-label into develop 2026-09-03 18:34:04 +08:00
Owner

功能 1:提取视频配音接口

POST /voices/extract-voice

接收视频文件,用 ffmpeg 提取人声并降噪,保存到配音素材库。

接口规格

  • 输入:视频文件(mp4/mov/webm/avi)+ project_id,最大 500MB
  • 处理
    1. 保存视频到临时文件
    2. ffmpeg 提取音频 + 降噪滤镜链:highpass=f=80 → afftdn=nf=-25 → lowpass=f=8000
    3. 编码为 mp3 192kbps 单声道 44.1kHz
    4. 上传到 OSS uploads/voice/extracted/{uuid}.mp3
    5. 创建 Asset 记录到用户配音素材库
    6. 清理临时文件
  • 输出{id, name, audio_url, duration, file_size, status, source}
  • metadatasource: "video_extract", original_video: 原文件名

错误处理

  • 非视频文件 → 400
  • 文件过大 → 413
  • ffmpeg 失败(无音轨等) → 422
  • 超时(>5分钟) → 504

功能 2:素材来源标识

在 Asset 的 metadata.source 字段标识来源:

来源 source 值 写入位置
TTS 保存 tts_job tts.py save_tts_job_to_library()(已有)
用户上传 upload ingest.py worker(本次新增)
视频提取 video_extract voices.py extract_voice_from_video()(本次新增)

AssetResponse.metadata 已包含此字段,前端可直接通过 asset.metadata.source 判断来源,区分 AI 生成配音和用户上传配音。

文件变更

  • apps/api/app/api/routes/voices.py — 新增 extract-voice 端点 + 辅助函数
  • apps/worker/worker_app/tasks/ingest.py — 上传素材写入 source=upload 标识
## 功能 1:提取视频配音接口 **POST /voices/extract-voice** 接收视频文件,用 ffmpeg 提取人声并降噪,保存到配音素材库。 ### 接口规格 - **输入**:视频文件(mp4/mov/webm/avi)+ project_id,最大 500MB - **处理**: 1. 保存视频到临时文件 2. ffmpeg 提取音频 + 降噪滤镜链:`highpass=f=80 → afftdn=nf=-25 → lowpass=f=8000` 3. 编码为 mp3 192kbps 单声道 44.1kHz 4. 上传到 OSS `uploads/voice/extracted/{uuid}.mp3` 5. 创建 Asset 记录到用户配音素材库 6. 清理临时文件 - **输出**:`{id, name, audio_url, duration, file_size, status, source}` - **metadata**:`source: "video_extract"`, `original_video: 原文件名` ### 错误处理 - 非视频文件 → 400 - 文件过大 → 413 - ffmpeg 失败(无音轨等) → 422 - 超时(>5分钟) → 504 ## 功能 2:素材来源标识 在 Asset 的 `metadata.source` 字段标识来源: | 来源 | source 值 | 写入位置 | |------|-----------|----------| | TTS 保存 | `tts_job` | `tts.py` save_tts_job_to_library()(已有) | | 用户上传 | `upload` | `ingest.py` worker(本次新增) | | 视频提取 | `video_extract` | `voices.py` extract_voice_from_video()(本次新增) | `AssetResponse.metadata` 已包含此字段,前端可直接通过 `asset.metadata.source` 判断来源,区分 AI 生成配音和用户上传配音。 ## 文件变更 - `apps/api/app/api/routes/voices.py` — 新增 extract-voice 端点 + 辅助函数 - `apps/worker/worker_app/tasks/ingest.py` — 上传素材写入 source=upload 标识
xiaoxia added 1 commit 2026-09-03 18:23:56 +08:00
feat(voice): 提取视频配音接口 + 素材来源标识
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
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 / Frontend Lint (pull_request) Has been skipped
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 / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped 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 E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m39s
AI Code Review / AI Code Review (pull_request) Failing after 1m44s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m45s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 4m43s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
5478be5c09
1. 新增 POST /voices/extract-voice 接口:
   - 接收视频文件(mp4/mov/webm/avi,最大 500MB)
   - ffmpeg 提取音频 + 人声降噪(highpass→afftdn→lowpass 滤镜链)
   - 编码为 mp3 192kbps 保存到配音素材库
   - 原视频不保留(临时文件处理后即清理)
   - 返回素材信息(id、时长、文件大小、URL)
   - metadata.source = 'video_extract'

2. 素材来源标识(metadata.source 字段):
   - TTS 保存:source='tts_job'(已有)
   - 用户上传:source='upload'(本次新增,写入 ingest worker)
   - 视频提取:source='video_extract'(本次新增)
   - metadata 已在 AssetResponse 中返回,前端可直接读取

🚀 预览环境已部署

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

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

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

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

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1654 | | 预览链接 | [https://pr-1654.preview.xiaoxiajianji.com](https://pr-1654.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
auto-approve-bot added 1 commit 2026-09-03 18:28:40 +08:00
style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
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 / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 22s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m47s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m52s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 2m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
AI Code Review / AI Code Review (pull_request) Failing after 3m46s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 3m52s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 7m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 22s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 49s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 40s
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 / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
fe0ceceeb6
Collaborator

【阻塞级判定】

  • 是否存在阻塞级问题:是
  • 阻塞级问题数量:3 个

📊 审查概览

  • 整体评价:需修改
  • 建议级问题数量:2 个

🔴 阻塞级问题(必须修复)

  1. [apps/api/app/api/routes/voices.py: 535] 参数 project_id 未被使用

    • 问题类型:逻辑bug
    • 问题描述:函数 extract_voice_from_video 接收了 project_id 表单参数,但在函数体内部完全未使用。代码逻辑是调用 _find_or_create_voice_library_for_extract,该函数会遍历用户的所有可访问项目并选取第一个匹配项。这导致用户无法指定将提取的配音存放到特定项目,违背了接口设计意图。
    • 修改建议:将 project_id 传递给 _find_or_create_voice_library_for_extract 函数,并修改该函数逻辑以优先使用指定的 project_id,而非遍历查找。
  2. [apps/worker/worker_app/tasks/ingest.py: 688] Metadata 覆盖导致数据丢失

    • 问题类型:数据损坏风险
    • 问题描述:在更新已存在的 Asset 记录时,使用了 asset.metadata = metadata 进行直接赋值。metadata 变量在当前上下文中仅包含 source: "upload" 等字段。如果该 Asset 是由 voices.py 创建的(包含 source: "video_extract"original_video 等元数据),此操作会清空原有的业务元数据,导致数据丢失。
    • 修改建议:使用字典合并(如 asset.metadata.update(metadata))或仅更新特定字段,避免全量覆盖。
  3. [apps/api/app/api/routes/voices.py: 564] 文件名处理存在路径遍历风险

    • 问题类型:安全漏洞
    • 问题描述:构造 video_path 时直接使用了 file.filenametmp_dir / f"...{file.filename}"。如果攻击者上传的文件名包含 ../(如 ../../evil.mp4),pathlib 解析后可能会将文件写入临时目录之外(如 /tmp/evil.mp4),虽然受限于系统权限,但仍存在污染系统临时目录或覆盖其他文件的风险。此外,特殊字符可能导致 ffmpeg 调用失败。
    • 修改建议:不要直接使用用户提供的文件名作为磁盘文件名。应使用 uuid 生成安全的临时文件名,原始文件名仅用于元数据记录或返回值。

💡 改进建议(不阻塞合并)

  1. [apps/worker/worker_app/tasks/ingest.py: 633, 669, 688] 重复的 Metadata 赋值

    • 具体内容:代码在 ingest_error 分支、新建 Asset 分支和更新 Asset 分支均设置了 metadata["source"] = "upload"。建议在函数入口处统一处理 metadata 的初始化或默认值,减少重复代码。
  2. [apps/api/app/api/routes/voices.py: 589] 依赖外部命令未做存在性检查

    • 具体内容:代码直接调用 subprocess.run(["ffmpeg", ...])。如果运行环境未安装 ffmpeg 或不在 PATH 中,会导致运行时错误。虽然这是环境配置问题,但在关键路径上增加预检查或捕获更具体的 FileNotFoundError 会更健壮。

良好实践

  • 使用了 subprocess.run 的列表参数形式调用外部命令,有效避免了 Shell 注入风险。
  • voices.py 中设置了 timeout=300,防止长时间处理阻塞服务。
  • 使用了 tempfile.mkdtemptry...finally 确保临时文件被清理,资源管理良好。
  • 对上传文件大小进行了流式检查(1MB chunk),防止内存溢出。

🤖 由 AI 代码审查机器人自动生成 | 2026-09-03 10:33:05 | 模型:

### 【阻塞级判定】 - 是否存在阻塞级问题:是 - 阻塞级问题数量:3 个 ### 📊 审查概览 - 整体评价:需修改 - 建议级问题数量:2 个 ### 🔴 阻塞级问题(必须修复) 1. **[apps/api/app/api/routes/voices.py: 535] 参数 `project_id` 未被使用** - 问题类型:逻辑bug - 问题描述:函数 `extract_voice_from_video` 接收了 `project_id` 表单参数,但在函数体内部完全未使用。代码逻辑是调用 `_find_or_create_voice_library_for_extract`,该函数会遍历用户的所有可访问项目并选取第一个匹配项。这导致用户无法指定将提取的配音存放到特定项目,违背了接口设计意图。 - 修改建议:将 `project_id` 传递给 `_find_or_create_voice_library_for_extract` 函数,并修改该函数逻辑以优先使用指定的 `project_id`,而非遍历查找。 2. **[apps/worker/worker_app/tasks/ingest.py: 688] Metadata 覆盖导致数据丢失** - 问题类型:数据损坏风险 - 问题描述:在更新已存在的 Asset 记录时,使用了 `asset.metadata = metadata` 进行直接赋值。`metadata` 变量在当前上下文中仅包含 `source: "upload"` 等字段。如果该 Asset 是由 `voices.py` 创建的(包含 `source: "video_extract"` 和 `original_video` 等元数据),此操作会清空原有的业务元数据,导致数据丢失。 - 修改建议:使用字典合并(如 `asset.metadata.update(metadata)`)或仅更新特定字段,避免全量覆盖。 3. **[apps/api/app/api/routes/voices.py: 564] 文件名处理存在路径遍历风险** - 问题类型:安全漏洞 - 问题描述:构造 `video_path` 时直接使用了 `file.filename`:`tmp_dir / f"...{file.filename}"`。如果攻击者上传的文件名包含 `../`(如 `../../evil.mp4`),`pathlib` 解析后可能会将文件写入临时目录之外(如 `/tmp/evil.mp4`),虽然受限于系统权限,但仍存在污染系统临时目录或覆盖其他文件的风险。此外,特殊字符可能导致 `ffmpeg` 调用失败。 - 修改建议:不要直接使用用户提供的文件名作为磁盘文件名。应使用 `uuid` 生成安全的临时文件名,原始文件名仅用于元数据记录或返回值。 ### 💡 改进建议(不阻塞合并) 1. **[apps/worker/worker_app/tasks/ingest.py: 633, 669, 688] 重复的 Metadata 赋值** - 具体内容:代码在 `ingest_error` 分支、新建 Asset 分支和更新 Asset 分支均设置了 `metadata["source"] = "upload"`。建议在函数入口处统一处理 `metadata` 的初始化或默认值,减少重复代码。 2. **[apps/api/app/api/routes/voices.py: 589] 依赖外部命令未做存在性检查** - 具体内容:代码直接调用 `subprocess.run(["ffmpeg", ...])`。如果运行环境未安装 ffmpeg 或不在 PATH 中,会导致运行时错误。虽然这是环境配置问题,但在关键路径上增加预检查或捕获更具体的 `FileNotFoundError` 会更健壮。 ### ✅ 良好实践 - 使用了 `subprocess.run` 的列表参数形式调用外部命令,有效避免了 Shell 注入风险。 - 在 `voices.py` 中设置了 `timeout=300`,防止长时间处理阻塞服务。 - 使用了 `tempfile.mkdtemp` 和 `try...finally` 确保临时文件被清理,资源管理良好。 - 对上传文件大小进行了流式检查(1MB chunk),防止内存溢出。 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-09-03 10:33:05 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
xiaoxia merged commit e6e4090f3c into develop 2026-09-03 18:34:04 +08:00

🗑️ 预览环境已清理

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

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

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