feat: 任务3.05 音色克隆API — CRUD + 状态管理 #165

Merged
xiaoxia merged 1 commits from feature/task-305-voice-clone-api into develop 2026-07-02 10:59:42 +08:00
Owner

任务 3.05:音色克隆 API

内容

  • 6 个 REST API 端点(CRUD + 状态轮询 + 重试)
  • VoiceCloneProfile 领域模型 + SQLAlchemy 适配器
  • 6 个 Use Case(Create/List/Get/GetStatus/Delete/Retry)
  • Alembic 迁移 019(voice_clone_profiles 表)
  • 19 个单元测试全部通过

延期说明

CosyVoice API 实际调用延期到 Task 3.07,新建克隆停留在 pending 状态。

## 任务 3.05:音色克隆 API ### 内容 - 6 个 REST API 端点(CRUD + 状态轮询 + 重试) - VoiceCloneProfile 领域模型 + SQLAlchemy 适配器 - 6 个 Use Case(Create/List/Get/GetStatus/Delete/Retry) - Alembic 迁移 019(voice_clone_profiles 表) - 19 个单元测试全部通过 ### 延期说明 CosyVoice API 实际调用延期到 Task 3.07,新建克隆停留在 pending 状态。
xiaoxia added 1 commit 2026-07-02 10:53:34 +08:00
feat: implement voice clone API (Task 3.05)
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 170h0m19s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 170h0m23s
Deploy / Deploy Staging (push) Failing after 170h2m52s
CI/CD Pipeline / Frontend Lint (push) Failing after 170h3m23s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 170h3m30s
c55dafdbb1
- Add VoiceCloneProfile CRUD endpoints:
  POST /api/v1/voice-clones (create clone task, status=pending)
  GET /api/v1/voice-clones (list with pagination + status filter)
  GET /api/v1/voice-clones/{id} (get details)
  GET /api/v1/voice-clones/{id}/status (polling endpoint)
  DELETE /api/v1/voice-clones/{id} (soft delete)
  POST /api/v1/voice-clones/{id}/retry (retry failed clone)
- Add VoiceCloneProfile SQLAlchemy model and repository adapter
- Add Alembic migration 019 for voice_clone_profiles table
- Add use cases: Create, List, Get, GetStatus, Delete, Retry
- Add Pydantic schemas for request/response validation
- Add 19 unit tests (all passing)
- CosyVoice API integration deferred to Task 3.07

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

PR #165 审查报告 — 任务 3.05 音色克隆 API

PR: #165
分支: feature/task-305-voice-clone-apidevelop
Commit: c55dafdb
变更: 9 文件,+930 / -0
结论: 有条件通过(0 P0 / 0 P1 / 2 P2 / 4 P3)
mergeable: True


一、变更文件清单

文件 行数 说明
apps/api/app/api/routes/voice_clones.py +180 6 个 REST 端点路由
apps/api/app/schemas/voice_clone.py +69 请求/响应 Schema
packages/adapters/sqlalchemy_impl/voice_clone_profile_repository.py +152 SQLAlchemy 适配器
packages/adapters/sqlalchemy_impl/models.py +23 VoiceCloneProfileModel ORM
packages/application/voice_clone/__init__.py +23 Use Case 包导出
packages/application/voice_clone/use_cases.py +132 6 个 Use Case
alembic/versions/019_add_voice_clone_profiles_table.py +52 数据库迁移
apps/api/app/api/router.py +6 路由注册
tests/unit/test_voice_clone_api.py +293 19 个单元测试

二、审查要点验证

1. Repository 正确实现端口接口

  • VoiceCloneProfileRepository(Protocol)定义 7 个方法:create / get / update / delete / list_by_user / count_by_user / find_by_voice_id
  • SQLAlchemyVoiceCloneProfileRepository 全部实现,方法签名完全匹配
  • 使用 Protocol 实现结构化子类型,符合 Hexagonal 架构

2. 状态机转换符合领域模型

  • 状态枚举:pending → processing → ready / failedfailed → pending(重试),ready/disabled 为终态
  • _VALID_TRANSITIONS 字典严格定义合法转换
  • transition_to() 方法校验非法转换并抛出 ValueError
  • 辅助方法:mark_processing() / mark_ready(voice_id) / mark_failed(error_message) / mark_disabled() / prepare_retry()

3. 重试逻辑正确

  • is_retryable 属性:status == FAILED and retry_count < max_retries
  • prepare_retry()retry_count += 1transition_to(PENDING) → 清除 error_messagevoice_id
  • Use Case 层先检查 is_retryable,不可重试抛出 VoiceCloneNotRetryableError
  • 路由层捕获异常返回 400

4. API 认证/鉴权正确

  • 所有 6 个端点都依赖 get_current_user 获取认证用户
  • Use Case 层额外验证 profile.user_id != user_id → 抛出 VoiceCloneNotFoundError(404)
  • 防止用户 A 访问/修改/删除用户 B 的克隆档案

5. 分页实现正确

  • 使用 skip(offset)+ limit 分页,默认 skip=0, limit=50, 最大 200
  • 同时返回 total(通过 count_by_user
  • Repository 层 list_by_user 支持 status 过滤 + 按 created_at desc 排序

6. 错误处理完善

  • 404:VoiceCloneNotFoundError → GET/{id} / GET/{id}/status / DELETE / POST/{id}/retry
  • 400:VoiceCloneNotRetryableError → POST/{id}/retry
  • 204:DELETE 成功返回 No Content

7. Alembic 迁移正确

  • revision = "019", down_revision = "018" 链路正确
  • 表结构 16 列,与 ORM Model 完全一致
  • user_idstatus 有索引
  • metadata 列使用 JSON 类型
  • created_at / updated_at 使用 server_default=sa.func.now()
  • downgrade 正确

8. 单元测试覆盖充分

  • 19 个测试用例,覆盖所有 6 个 Use Case
  • 包含正常路径 + 异常路径(不存在、用户不匹配、不可重试状态)
  • 重试测试覆盖 pending/ready/processing 三种不可重试状态

9. Hexagonal 架构分层合理

domain (VoiceCloneProfile)
  ↓ 依赖
ports (VoiceCloneProfileRepository Protocol)
  ↓ 实现
adapters (SQLAlchemyVoiceCloneProfileRepository)
  ↑ 调用
application (Use Cases)
  ↑ 编排
api (Routes + Schemas)

三、发现的问题

P2-1:分页参数风格不一致(skip/limit vs page/page_size)

  • 文件: voice_clones.py 第 96-97 行
  • 现状: 使用 skip + limit 分页
  • 问题: 项目其他 API(edit-plans、voices)统一使用 page + page_size,风格不一致
  • 影响: 前端需要为不同 API 适配不同的分页参数格式
  • 建议: 统一为 page(从 1 开始)+ page_size,在 Use Case 层转换为 skip = (page - 1) * page_size

P2-2:DELETE 缺少关联引用检查

  • 文件: use_cases.py DeleteVoiceCloneUseCase
  • 现状: 直接软删除,不检查是否有 TTS 任务正在引用该音色
  • 问题: 如果 ready 状态的音色正被视频生成任务引用,删除后可能导致引用断裂
  • 影响: 当前阶段无实际 CosyVoice 调用,暂不影响;但后续任务 3.07 集成时需注意
  • 建议: 后续可增加 is_in_use 检查或在 voice_id 维度做引用计数

P3-1:metadata_ 命名风格

  • 文件: voice_clone.py Schema + models.py ORM
  • 现状: Python 属性名 metadata_,通过 alias="metadata" 映射到 JSON 字段 metadata
  • 评价: 功能正确,但 metadata_ 后缀命名略不直观;项目中其他 Model 使用 extra_meta = Column("metadata", ...) 风格
  • 建议: 可考虑与现有 Model 保持一致使用 extra_meta 命名

P3-2:VoiceCloneStatus 枚举不包含 "deleted"

  • 文件: voice_clone_profile.py + voice_clone_profile_repository.py
  • 现状: 领域模型定义 5 种状态(pending/processing/ready/failed/disabled),但 Repository 软删除设置 status = "deleted",不在枚举中
  • 评价: 由于 get()list_by_user() 已过滤 status != "deleted",实际运行中不会触发问题。但设计上存在类型不一致
  • 建议: 可将 "deleted" 加入枚举,或在 _model_to_entity 中增加断言

P3-3:source_audio_url 缺少格式校验

  • 文件: voice_clone.py CreateVoiceCloneRequest
  • 现状: source_audio_url 默认为空字符串,无 URL 格式校验
  • 评价: 当前阶段不实现 CosyVoice API 调用(延期到 3.07),暂可接受
  • 建议: 后续集成时增加 URL 格式校验 + 文件类型/大小限制

P3-4:缺少硬删除/数据清理机制

  • 现状: 仅支持软删除,无永久清理接口
  • 评价: 对于 GDPR 合规或数据归档场景,可能需要硬删除能力
  • 建议: 后续可增加管理员专用的硬删除端点或定时清理任务

四、亮点

  1. 领域模型设计优秀:状态机 + 合法转换表 + 工厂方法 + 值语义 dataclass(slots=True),代码清晰且安全
  2. 权限检查双层保障:路由层 get_current_user + Use Case 层 user_id 校验
  3. 测试覆盖全面:19 个测试覆盖所有 Use Case 的正常和异常路径
  4. 软删除 + 过滤一致性:delete/list/get 都正确处理 "deleted" 状态
  5. Schema 设计合理VoiceCloneStatusResponse 轻量版用于轮询,VoiceCloneProfileResponse 完整版用于详情

五、总结

级别 数量 说明
P0 0
P1 0
P2 2 分页风格不一致、缺少关联引用检查
P3 4 命名风格、枚举完整性、URL 校验、硬删除

结论: 有条件通过 — 代码质量高,架构合理,核心功能完整。P2 问题不阻塞合并,建议后续迭代修复。

可合并: mergeable=True,修复 P2 后合并或合并后修复均可。

# PR #165 审查报告 — 任务 3.05 音色克隆 API **PR**: [#165](https://git.xiaoxiaianji.com/xiaoxia/xiaoxia-saas/pulls/165) **分支**: `feature/task-305-voice-clone-api` → `develop` **Commit**: `c55dafdb` **变更**: 9 文件,+930 / -0 **结论**: ✅ **有条件通过**(0 P0 / 0 P1 / 2 P2 / 4 P3) **mergeable**: ✅ True --- ## 一、变更文件清单 | 文件 | 行数 | 说明 | |------|------|------| | `apps/api/app/api/routes/voice_clones.py` | +180 | 6 个 REST 端点路由 | | `apps/api/app/schemas/voice_clone.py` | +69 | 请求/响应 Schema | | `packages/adapters/sqlalchemy_impl/voice_clone_profile_repository.py` | +152 | SQLAlchemy 适配器 | | `packages/adapters/sqlalchemy_impl/models.py` | +23 | VoiceCloneProfileModel ORM | | `packages/application/voice_clone/__init__.py` | +23 | Use Case 包导出 | | `packages/application/voice_clone/use_cases.py` | +132 | 6 个 Use Case | | `alembic/versions/019_add_voice_clone_profiles_table.py` | +52 | 数据库迁移 | | `apps/api/app/api/router.py` | +6 | 路由注册 | | `tests/unit/test_voice_clone_api.py` | +293 | 19 个单元测试 | ## 二、审查要点验证 ### ✅ 1. Repository 正确实现端口接口 - `VoiceCloneProfileRepository`(Protocol)定义 7 个方法:create / get / update / delete / list_by_user / count_by_user / find_by_voice_id - `SQLAlchemyVoiceCloneProfileRepository` 全部实现,方法签名完全匹配 - 使用 `Protocol` 实现结构化子类型,符合 Hexagonal 架构 ### ✅ 2. 状态机转换符合领域模型 - 状态枚举:`pending → processing → ready / failed`,`failed → pending`(重试),`ready/disabled` 为终态 - `_VALID_TRANSITIONS` 字典严格定义合法转换 - `transition_to()` 方法校验非法转换并抛出 `ValueError` - 辅助方法:`mark_processing()` / `mark_ready(voice_id)` / `mark_failed(error_message)` / `mark_disabled()` / `prepare_retry()` ### ✅ 3. 重试逻辑正确 - `is_retryable` 属性:`status == FAILED and retry_count < max_retries` - `prepare_retry()`:`retry_count += 1` → `transition_to(PENDING)` → 清除 `error_message` 和 `voice_id` - Use Case 层先检查 `is_retryable`,不可重试抛出 `VoiceCloneNotRetryableError` - 路由层捕获异常返回 400 ### ✅ 4. API 认证/鉴权正确 - 所有 6 个端点都依赖 `get_current_user` 获取认证用户 - Use Case 层额外验证 `profile.user_id != user_id` → 抛出 `VoiceCloneNotFoundError`(404) - 防止用户 A 访问/修改/删除用户 B 的克隆档案 ### ✅ 5. 分页实现正确 - 使用 `skip`(offset)+ `limit` 分页,默认 skip=0, limit=50, 最大 200 - 同时返回 `total`(通过 `count_by_user`) - Repository 层 `list_by_user` 支持 status 过滤 + 按 `created_at desc` 排序 ### ✅ 6. 错误处理完善 - 404:`VoiceCloneNotFoundError` → GET/{id} / GET/{id}/status / DELETE / POST/{id}/retry - 400:`VoiceCloneNotRetryableError` → POST/{id}/retry - 204:DELETE 成功返回 No Content ### ✅ 7. Alembic 迁移正确 - `revision = "019"`, `down_revision = "018"` 链路正确 - 表结构 16 列,与 ORM Model 完全一致 - `user_id` 和 `status` 有索引 - `metadata` 列使用 `JSON` 类型 - `created_at` / `updated_at` 使用 `server_default=sa.func.now()` - downgrade 正确 ### ✅ 8. 单元测试覆盖充分 - 19 个测试用例,覆盖所有 6 个 Use Case - 包含正常路径 + 异常路径(不存在、用户不匹配、不可重试状态) - 重试测试覆盖 pending/ready/processing 三种不可重试状态 ### ✅ 9. Hexagonal 架构分层合理 ``` domain (VoiceCloneProfile) ↓ 依赖 ports (VoiceCloneProfileRepository Protocol) ↓ 实现 adapters (SQLAlchemyVoiceCloneProfileRepository) ↑ 调用 application (Use Cases) ↑ 编排 api (Routes + Schemas) ``` ## 三、发现的问题 ### P2-1:分页参数风格不一致(skip/limit vs page/page_size) - **文件**: `voice_clones.py` 第 96-97 行 - **现状**: 使用 `skip` + `limit` 分页 - **问题**: 项目其他 API(edit-plans、voices)统一使用 `page` + `page_size`,风格不一致 - **影响**: 前端需要为不同 API 适配不同的分页参数格式 - **建议**: 统一为 `page`(从 1 开始)+ `page_size`,在 Use Case 层转换为 `skip = (page - 1) * page_size` ### P2-2:DELETE 缺少关联引用检查 - **文件**: `use_cases.py` `DeleteVoiceCloneUseCase` - **现状**: 直接软删除,不检查是否有 TTS 任务正在引用该音色 - **问题**: 如果 `ready` 状态的音色正被视频生成任务引用,删除后可能导致引用断裂 - **影响**: 当前阶段无实际 CosyVoice 调用,暂不影响;但后续任务 3.07 集成时需注意 - **建议**: 后续可增加 `is_in_use` 检查或在 `voice_id` 维度做引用计数 ### P3-1:`metadata_` 命名风格 - **文件**: `voice_clone.py` Schema + `models.py` ORM - **现状**: Python 属性名 `metadata_`,通过 `alias="metadata"` 映射到 JSON 字段 `metadata` - **评价**: 功能正确,但 `metadata_` 后缀命名略不直观;项目中其他 Model 使用 `extra_meta = Column("metadata", ...)` 风格 - **建议**: 可考虑与现有 Model 保持一致使用 `extra_meta` 命名 ### P3-2:VoiceCloneStatus 枚举不包含 "deleted" - **文件**: `voice_clone_profile.py` + `voice_clone_profile_repository.py` - **现状**: 领域模型定义 5 种状态(pending/processing/ready/failed/disabled),但 Repository 软删除设置 `status = "deleted"`,不在枚举中 - **评价**: 由于 `get()` 和 `list_by_user()` 已过滤 `status != "deleted"`,实际运行中不会触发问题。但设计上存在类型不一致 - **建议**: 可将 "deleted" 加入枚举,或在 `_model_to_entity` 中增加断言 ### P3-3:source_audio_url 缺少格式校验 - **文件**: `voice_clone.py` `CreateVoiceCloneRequest` - **现状**: `source_audio_url` 默认为空字符串,无 URL 格式校验 - **评价**: 当前阶段不实现 CosyVoice API 调用(延期到 3.07),暂可接受 - **建议**: 后续集成时增加 URL 格式校验 + 文件类型/大小限制 ### P3-4:缺少硬删除/数据清理机制 - **现状**: 仅支持软删除,无永久清理接口 - **评价**: 对于 GDPR 合规或数据归档场景,可能需要硬删除能力 - **建议**: 后续可增加管理员专用的硬删除端点或定时清理任务 ## 四、亮点 1. **领域模型设计优秀**:状态机 + 合法转换表 + 工厂方法 + 值语义 dataclass(slots=True),代码清晰且安全 2. **权限检查双层保障**:路由层 `get_current_user` + Use Case 层 `user_id` 校验 3. **测试覆盖全面**:19 个测试覆盖所有 Use Case 的正常和异常路径 4. **软删除 + 过滤一致性**:delete/list/get 都正确处理 "deleted" 状态 5. **Schema 设计合理**:`VoiceCloneStatusResponse` 轻量版用于轮询,`VoiceCloneProfileResponse` 完整版用于详情 ## 五、总结 | 级别 | 数量 | 说明 | |------|------|------| | P0 | 0 | — | | P1 | 0 | — | | P2 | 2 | 分页风格不一致、缺少关联引用检查 | | P3 | 4 | 命名风格、枚举完整性、URL 校验、硬删除 | **结论**: ✅ **有条件通过** — 代码质量高,架构合理,核心功能完整。P2 问题不阻塞合并,建议后续迭代修复。 **可合并**: ✅ mergeable=True,修复 P2 后合并或合并后修复均可。
xiaoxia merged commit cf0bd4ef40 into develop 2026-07-02 10:59:42 +08:00
Sign in to join this conversation.