Compare commits

...

10 Commits

Author SHA1 Message Date
xiaoxia 099134d88b fix: 修复 CoverTemplate 导入路径,修复 TS type check 失败
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 47s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 48s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 55s
AI Code Review / AI Code Review (pull_request) Failing after 1m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m41s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m45s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m20s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 59s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m16s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 910h31m47s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 910h31m48s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 910h31m49s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 910h31m49s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 910h31m49s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 910h35m20s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 910h35m21s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 910h35m22s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 910h36m5s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 910h36m6s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 910h36m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 910h36m8s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 910h36m10s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 911h5m14s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 911h5m15s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 911h9m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 911h9m35s
2026-08-09 16:50:49 +08:00
xiaoxia b9144f7b97 fix: 清理封面 hook 未使用的变量和导入,修复 Frontend Lint
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m1s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m5s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m51s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m0s
AI Code Review / AI Code Review (pull_request) Failing after 3m44s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m33s
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 910h52m21s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 910h52m22s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 910h52m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 910h52m23s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 910h52m23s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 910h56m10s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 910h56m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 910h56m52s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 910h56m54s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 910h56m57s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 910h56m57s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 910h56m57s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 911h25m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 911h25m49s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 911h29m34s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 911h30m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 911h30m20s
2026-08-09 16:30:04 +08:00
xiaoxia 18c503e0af feat: 封面设置区域 UI 改造
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 43s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 56s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 30s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m58s
AI Code Review / AI Code Review (pull_request) Failing after 2m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m4s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m32s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 911h8m15s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 911h8m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 911h8m16s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 911h8m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 911h8m17s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 911h11m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 911h11m7s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 911h11m47s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 911h11m47s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 911h11m49s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 911h11m51s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 911h11m51s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 911h11m51s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 911h41m41s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 911h41m42s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 911h44m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 911h45m12s
- 改造 Step6CoverSettings 顶层区域:删除三tab布局,改为双按钮+模板名+预览
- 新增 CoverSettingsModal 封面设置弹窗(4列网格模板卡片)
- 新增 CoverEditorModal 自定义封面编辑器弹窗(折叠面板+画布预览)
- 新增 CoverTemplate 类型定义及 MOCK_COVER_TEMPLATES 常量
- 更新 useStep6Cover hook 添加弹窗状态和模板操作方法
- 新增封面设置相关 CSS 样式
- TypeScript 编译通过,Prettier 格式化完成
2026-08-09 16:14:36 +08:00
xiaoxia a75fa1cd93 fix: Worker 配音下载 fallback 到 voice_ids 修复预览无声 (#1320)
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 40s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m24s
CI/CD Pipeline / Integration Tests (push) Successful in 1m21s
CI/CD Pipeline / Unit Tests (push) Successful in 8m50s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m29s
CI/CD Pipeline / Canary Release to Production (push) Failing after 913h2m53s
CI/CD Pipeline / Production Browser E2E (push) Failing after 913h10m55s
CI/CD Pipeline / CI Gate (push) Failing after 913h10m56s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 913h10m56s
CI/CD Pipeline / Build Production API Image (push) Failing after 913h10m57s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 913h19m45s
CI/CD Pipeline / PR Build API Image (push) Failing after 913h19m47s
CI/CD Pipeline / Frontend Lint (push) Failing after 913h19m48s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 913h19m49s
CI/CD Pipeline / Deploy Production (push) Failing after 913h44m21s
CI/CD Pipeline / Build Production Web Image (push) Failing after 913h44m22s
CI/CD Pipeline / PR Build Web Image (push) Failing after 913h53m11s
2026-08-09 14:07:13 +08:00
xiaoxia d9c5281f46 feat: 预览生成 API 传入 voice_library_id(配音素材 ID) (#1314)
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 50s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m35s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 3m52s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m33s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Unit Tests (push) Successful in 8m39s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m23s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m27s
CI/CD Pipeline / Canary Release to Production (push) Failing after 914h39m13s
CI/CD Pipeline / Deploy Production (push) Failing after 914h47m19s
CI/CD Pipeline / CI Gate (push) Failing after 914h47m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 914h47m20s
CI/CD Pipeline / Build Production API Image (push) Failing after 914h47m20s
CI/CD Pipeline / PR Build Web Image (push) Failing after 914h55m59s
CI/CD Pipeline / PR Build API Image (push) Failing after 914h56m0s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 914h56m2s
CI/CD Pipeline / Production Browser E2E (push) Failing after 915h20m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 915h20m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 915h29m24s
CI/CD Pipeline / Frontend Lint (push) Failing after 915h29m27s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-09 12:31:00 +08:00
xiaoxia 6b5ec0a2f1 Merge pull request 'fix: 预览生成接口透传 voice_library_id 修复预览无声问题' (#1313) from fix/preview-voice-library-id into develop
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m10s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m15s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m14s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m38s
CI/CD Pipeline / Integration Tests (push) Successful in 1m28s
CI/CD Pipeline / Unit Tests (push) Successful in 8m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m27s
CI/CD Pipeline / Canary Release to Production (push) Failing after 915h37m3s
CI/CD Pipeline / Deploy Production (push) Failing after 915h45m21s
CI/CD Pipeline / CI Gate (push) Failing after 915h45m21s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 915h45m21s
CI/CD Pipeline / Build Production API Image (push) Failing after 915h45m21s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 915h54m3s
CI/CD Pipeline / PR Build API Image (push) Failing after 915h54m5s
CI/CD Pipeline / Frontend Lint (push) Failing after 915h54m5s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 915h54m9s
CI/CD Pipeline / Production Browser E2E (push) Failing after 916h18m45s
CI/CD Pipeline / Build Production Web Image (push) Failing after 916h18m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 916h27m28s
2026-08-09 11:32:53 +08:00
CI Bot de30703d41 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 43s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m33s
AI Code Review / AI Code Review (pull_request) Successful in 1m34s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m43s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m53s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m54s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m28s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m37s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 48s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 49s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 915h54m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 915h54m44s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 915h54m45s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 915h54m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 916h3m3s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 916h3m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 916h3m7s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 916h3m3s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 916h3m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 916h3m5s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 916h28m9s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 916h28m10s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 916h36m32s
2026-08-09 03:23:53 +00:00
xiaoxia 68e41f6173 fix: 预览生成接口透传 voice_library_id 修复预览无声问题
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 43s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 55s
AI Code Review / AI Code Review (pull_request) Successful in 1m12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m23s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m52s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (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 Approve on CI Green (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 916h4m36s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 916h4m38s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 916h5m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 916h5m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 916h5m16s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 916h5m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 916h5m18s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 916h38m2s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 916h38m39s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 916h38m42s
根因:预览生成接口(POST /preview)硬编码 voice_library_id="",
导致用户选择的上传音频从未传递给 Worker,渲染时没有音频可混入。

修复:
- CreatePreviewGenerationTaskRequest 增加 voice_library_id 字段
- 预览端点将 request.voice_library_id 透传给 CreateGenerationTaskCommand
- 确认生成接口已自动继承源预览任务的 voice_library_id
- 新增 4 个单元测试验证 schema 字段

下游链路(Worker 下载音频 → 渲染引擎混音)无需修改,
只需前端在创建预览任务时传入 voice_library_id 即可。
2026-08-09 11:21:08 +08:00
xiaoxia 2c4fabb585 feat: 前端对接确认生成 API (POST /tasks/{task_id}/confirm) (#1310)
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m40s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m20s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 3m22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m28s
CI/CD Pipeline / Unit Tests (push) Successful in 8m25s
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m26s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m31s
CI/CD Pipeline / Canary Release to Production (push) Failing after 917h57m24s
CI/CD Pipeline / Deploy Production (push) Failing after 918h7m33s
CI/CD Pipeline / CI Gate (push) Failing after 918h7m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 918h7m33s
CI/CD Pipeline / Build Production API Image (push) Failing after 918h7m33s
CI/CD Pipeline / PR Build API Image (push) Failing after 918h15m59s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 918h15m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 918h16m0s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 918h16m1s
CI/CD Pipeline / Production Browser E2E (push) Failing after 918h40m57s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 918h40m58s
CI/CD Pipeline / PR Build Web Image (push) Failing after 918h49m23s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-09 09:11:01 +08:00
xiaoxia c79022bc2c Merge pull request 'fix: 确认生成 API 适配 develop 分支(同步 main PR #1308)' (#1312) from fix/confirm-generation-api-develop into develop
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m7s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m12s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m50s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m46s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m56s
CI/CD Pipeline / Integration Tests (push) Successful in 1m30s
CI/CD Pipeline / Unit Tests (push) Successful in 8m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m23s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m28s
CI/CD Pipeline / Canary Release to Production (push) Failing after 928h57m34s
CI/CD Pipeline / Deploy Production (push) Failing after 929h5m47s
CI/CD Pipeline / CI Gate (push) Failing after 929h5m48s
CI/CD Pipeline / Build Production Web Image (push) Failing after 929h5m48s
CI/CD Pipeline / Build Production API Image (push) Failing after 929h5m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 929h13m59s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 929h13m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 929h13m59s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 929h14m2s
CI/CD Pipeline / Production Browser E2E (push) Failing after 929h39m11s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 929h39m12s
CI/CD Pipeline / PR Build Web Image (push) Failing after 929h47m21s
2026-08-08 22:13:01 +08:00
20 changed files with 893 additions and 160 deletions
@@ -312,7 +312,7 @@ def create_preview_generation_task(
project_id="",
asset_library_id="",
strategy_id=strategy_id,
voice_library_id="",
voice_library_id=request.voice_library_id,
template_id=request.template_id,
asset_ids=list(request.asset_ids),
title_ids=list(request.title_ids),
+3
View File
@@ -161,6 +161,9 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
asset_ids: list[str] = Field(default_factory=list)
title_ids: list[str] = Field(default_factory=list)
voice_ids: list[str] = Field(default_factory=list)
voice_library_id: str = Field(
default="", description="配音素材库ID(用户上传的音频或AI配音),对应配音选择页面选择的配音素材"
)
video_title: str = Field(default="", description="生成视频的标题/名称,为空则使用默认命名")
duration: float = Field(default=0.0, ge=0, description="期望视频时长(秒),0 表示由模板决定")
video_ratio: str = Field(default="", description="视频比例,如 16:9 / 9:16,为空使用模板默认")
+14
View File
@@ -0,0 +1,14 @@
import apiClient from "../client"
import type { ConfirmGenerationRequest, ConfirmGenerationResponse } from "./types"
/** 确认生成 — 基于预览任务创建正式生成任务 */
export const confirmGeneration = async (
taskId: string,
params: ConfirmGenerationRequest,
): Promise<ConfirmGenerationResponse> => {
const response = await apiClient.post<ConfirmGenerationResponse>(
`/tasks/${taskId}/confirm`,
params,
)
return response.data
}
+4
View File
@@ -3,6 +3,10 @@ export type {
CreatePreviewRequest,
CreatePreviewResponse,
PreviewTaskResponse,
ConfirmGenerationRequest,
ConfirmGenerationResponse,
ConfirmGenerationTaskItem,
} from "./types"
export { createPreview, getPreviewStatus } from "./preview"
export { confirmGeneration } from "./confirm"
+46
View File
@@ -7,6 +7,8 @@ export interface CreatePreviewRequest {
asset_ids: string[]
title_ids?: string[]
voice_ids?: string[]
/** 配音素材库ID(用户上传的音频或AI配音),对应配音选择页面选择的配音素材 */
voice_library_id?: string
video_title?: string
duration?: number
video_ratio?: string
@@ -45,3 +47,47 @@ export interface PreviewTaskResponse {
finished_at?: string
generate_duration?: number
}
/** 确认生成请求体 — 基于预览任务创建正式生成任务 */
export interface ConfirmGenerationRequest {
/** 输出视频宽度,默认 1080 */
output_width?: number
/** 输出视频高度,默认 1920 */
output_height?: number
/** 自定义封面图片 URL */
cover_url?: string
/** 自定义视频标题 */
custom_title?: string
}
/** 确认生成响应 */
export interface ConfirmGenerationResponse {
items: ConfirmGenerationTaskItem[]
total: number
}
/** 确认生成返回的任务项 */
export interface ConfirmGenerationTaskItem {
id: string
project_id: string
asset_library_id: string
strategy_id: string
voice_library_id: string
template_id: string
asset_ids: string[]
title_ids: string[]
voice_ids: string[]
source_edit_plan_id: string
asset_select_mode: string
batch_id: string
is_preview: boolean
source_task_id: string
output_width: number
output_height: number
cover_url: string
custom_title: string
status: string
progress: number
result_count: number
error_message: string
}
@@ -30,3 +30,20 @@ export const DEFAULT_COVER_CONFIG: CoverConfig = {
ai_suggested_time: null,
thumbnail_url: "",
}
/** 封面模板 */
export interface CoverTemplate {
id: string
name: string
thumbnail_url: string
is_system: boolean
created_at: string
config?: {
background_enabled?: boolean
background_color?: string
portrait_enabled?: boolean
title_text?: string
subtitle_text?: string
mask_enabled?: boolean
}
}
@@ -72,7 +72,7 @@ export {
} from "./sticker"
/* 封面 */
export { type CoverMode, type CoverConfig, DEFAULT_COVER_CONFIG } from "./cover"
export { type CoverMode, type CoverConfig, DEFAULT_COVER_CONFIG, type CoverTemplate } from "./cover"
/* 片段数据 */
export { type ClipType, type ClipData } from "./clip"
@@ -106,6 +106,7 @@ const GeneratePage: React.FC = () => {
duration,
videoRatio,
voiceIds: previewVoiceIds,
voiceLibraryId: selectedVoice || undefined,
previewCount,
})
@@ -1,9 +1,9 @@
import React, { useEffect, useRef } from "react"
import React from "react"
import type { CoverConfig } from "../../editing-planner/types"
import { useStep6Cover } from "../hooks/useStep6Cover"
import { CoverModeSelector } from "./cover-settings/CoverModeSelector"
import { FrameCoverPicker } from "./cover-settings/FrameCoverPicker"
import { UploadCoverPicker } from "./cover-settings/UploadCoverPicker"
import Button from "@/components/ui/Button"
import CoverSettingsModal from "./cover-settings/CoverSettingsModal"
import CoverEditorModal from "./cover-settings/CoverEditorModal"
interface Step6CoverSettingsProps {
coverSettings: CoverConfig
@@ -18,15 +18,18 @@ interface Step6CoverSettingsProps {
const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
const {
coverSettings,
formatTime,
toggleEnabled,
setMode,
setFrameTime,
handleUpload,
generateAutoCover,
totalDuration,
COVER_MODE_LABELS,
COVER_MODE_ICONS,
showCoverSettings,
setShowCoverSettings,
showCoverEditor,
setShowCoverEditor,
selectedTemplateId,
selectedTemplateName,
editingTemplate,
coverTemplates,
handleSelectTemplate,
handleEditTemplate,
handleSaveTemplate,
} = useStep6Cover({
coverSettings: props.coverSettings,
onCoverSettingsChange: props.onCoverSettingsChange,
@@ -35,32 +38,9 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
selectedTemplate: props.selectedTemplate,
})
// 进入 auto 模式时自动触发智能封面生成
const autoTriggeredRef = useRef(false)
useEffect(() => {
// 切换模式、禁用封面或素材变更时重置触发标记
if (coverSettings.mode !== "auto" || !coverSettings.enabled) {
autoTriggeredRef.current = false
return
}
// 有素材且未生成过封面时自动触发
if (
coverSettings.mode === "auto" &&
!coverSettings.thumbnail_url &&
!autoTriggeredRef.current &&
props.assetIds &&
props.assetIds.length > 0
) {
autoTriggeredRef.current = true
generateAutoCover()
}
}, [
coverSettings.enabled,
coverSettings.mode,
coverSettings.thumbnail_url,
generateAutoCover,
props.assetIds,
])
const handleAutoGenerate = () => {
generateAutoCover()
}
// 预览图:优先 thumbnail_url,其次 upload_url
const previewUrl = coverSettings.thumbnail_url || coverSettings.upload_url
@@ -69,70 +49,49 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<div className="xx-form-section">
<h3>🖼️ 选择封面</h3>
<div className="xx-cover-header">
<span className="xx-cover-header-label">启用自定义封面</span>
<label className="xx-switch">
<input
type="checkbox"
checked={coverSettings.enabled}
onChange={(e) => toggleEnabled(e.target.checked)}
/>
<span className="xx-switch-slider" />
</label>
<div className="xx-cover-actions">
<Button buttonType="primary" onClick={handleAutoGenerate}>
✨ 自动生成封面
</Button>
<Button buttonType="ghost" onClick={() => setShowCoverSettings(true)}>
⚙️ 封面设置
</Button>
</div>
{coverSettings.enabled && (
<>
<div className="xx-section-title">封面来源</div>
<CoverModeSelector
mode={coverSettings.mode}
onModeChange={setMode}
modeLabels={COVER_MODE_LABELS}
modeIcons={COVER_MODE_ICONS}
/>
<div className="xx-cover-selected-template">已选模板: {selectedTemplateName}</div>
{coverSettings.mode === "auto" && (
<div className="xx-cover-auto">
<div className="xx-cover-auto-badge">
<span style={{ fontSize: 24 }}>🤖</span>
<span>AI 智能选帧</span>
</div>
</div>
)}
{coverSettings.mode === "frame" && (
<FrameCoverPicker
frameTime={coverSettings.frame_time}
totalDuration={totalDuration}
formatTime={formatTime}
onFrameTimeChange={setFrameTime}
/>
)}
{coverSettings.mode === "upload" && (
<UploadCoverPicker uploadUrl={coverSettings.upload_url} onUpload={handleUpload} />
)}
<div className="xx-section-title">封面预览</div>
<div className="xx-cover-preview-box">
{previewUrl ? (
<img src={previewUrl} alt="封面预览" className="xx-cover-preview-img" />
) : (
<div className="xx-cover-preview-placeholder">
<span style={{ fontSize: 28 }}>🖼️</span>
<span>
{coverSettings.mode === "auto"
? "AI 正在选择..."
: coverSettings.mode === "frame"
? `帧 ${formatTime(coverSettings.frame_time)}`
: "未上传封面"}
</span>
</div>
)}
<div className="xx-cover-preview-ratio">9:16</div>
<div className="xx-section-title">封面预览</div>
<div className="xx-cover-preview-box">
{previewUrl ? (
<img src={previewUrl} alt="封面预览" className="xx-cover-preview-img" />
) : (
<div className="xx-cover-preview-placeholder">
<span style={{ fontSize: 28 }}>🖼️</span>
<span>点击"自动生成封面"或选择模板</span>
</div>
</>
)}
)}
<div className="xx-cover-preview-ratio">9:16</div>
</div>
<CoverSettingsModal
open={showCoverSettings}
onClose={() => setShowCoverSettings(false)}
templates={coverTemplates}
selectedTemplateId={selectedTemplateId}
onSelectTemplate={handleSelectTemplate}
onEditTemplate={handleEditTemplate}
onCreateNew={() => {
setShowCoverSettings(false)
setShowCoverEditor(true)
}}
/>
<CoverEditorModal
open={showCoverEditor}
onClose={() => setShowCoverEditor(false)}
template={editingTemplate}
onSave={handleSaveTemplate}
/>
</div>
)
}
@@ -0,0 +1,125 @@
import React, { useState } from "react"
import type { CoverTemplate } from "../../../editing-planner/types"
import Modal from "@/components/ui/Modal"
import Button from "@/components/ui/Button"
interface CoverEditorModalProps {
open: boolean
onClose: () => void
template: CoverTemplate | null
onSave: (template: CoverTemplate) => void
}
interface SectionState {
basic: boolean
portrait: boolean
background: boolean
title: boolean
subtitle: boolean
mask: boolean
}
const CoverEditorModal: React.FC<CoverEditorModalProps> = ({ open, onClose, template, onSave }) => {
const [name, setName] = useState(template?.name || "")
const [sections, setSections] = useState<SectionState>({
basic: true,
portrait: false,
background: false,
title: true,
subtitle: true,
mask: false,
})
const toggleSection = (key: keyof SectionState) => {
setSections((prev) => ({ ...prev, [key]: !prev[key] }))
}
const handleSave = () => {
if (!template) return
onSave({ ...template, name })
onClose()
}
return (
<Modal
open={open}
onCancel={onClose}
width={1000}
title="自定义封面编辑器"
centered
footer={null}
>
<div className="xx-cover-editor-header">
<input
className="xx-cover-editor-name-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入模板名称"
/>
<div className="xx-cover-editor-header-actions">
<Button buttonType="ghost" onClick={onClose}>
取消
</Button>
<Button buttonType="primary" onClick={handleSave}>
保存模板
</Button>
</div>
</div>
<div className="xx-cover-editor-layout">
{/* 左侧折叠面板 */}
<div className="xx-cover-editor-left">
{[
{ key: "basic" as const, label: "基础设置" },
{ key: "portrait" as const, label: "人像设置" },
{ key: "background" as const, label: "背景设置", toggle: true },
{ key: "title" as const, label: "主标题" },
{ key: "subtitle" as const, label: "副标题" },
{ key: "mask" as const, label: "蒙版", toggle: true },
].map((item) => (
<div key={item.key} className="xx-cover-editor-section">
<div
className="xx-cover-editor-section-header"
onClick={() => toggleSection(item.key)}
>
<span>{item.label}</span>
<span>{sections[item.key] ? "▾" : "▸"}</span>
</div>
{sections[item.key] && (
<div className="xx-cover-editor-section-body">
{item.toggle ? (
<label style={{ display: "flex", alignItems: "center", gap: 6 }}>
<input type="checkbox" defaultChecked={false} />
已开启
</label>
) : (
<span style={{ color: "var(--text-tertiary)" }}>暂无配置项</span>
)}
</div>
)}
</div>
))}
</div>
{/* 右侧画布预览 */}
<div className="xx-cover-editor-right">
<div className="xx-cover-editor-canvas">
{/* 人像占位 */}
<div className="xx-cover-editor-portrait">
{/* 四角拖拽手柄 */}
<span className="xx-cover-editor-handle" style={{ top: -4, left: -4 }} />
<span className="xx-cover-editor-handle" style={{ top: -4, right: -4 }} />
<span className="xx-cover-editor-handle" style={{ bottom: -4, left: -4 }} />
<span className="xx-cover-editor-handle" style={{ bottom: -4, right: -4 }} />
</div>
{/* 文字占位 */}
<div className="xx-cover-editor-title-placeholder">主标题文字</div>
<div className="xx-cover-editor-subtitle-placeholder">副标题文字</div>
</div>
</div>
</div>
</Modal>
)
}
export default CoverEditorModal
@@ -0,0 +1,81 @@
import React from "react"
import type { CoverTemplate } from "../../../editing-planner/types"
import Modal from "@/components/ui/Modal"
import Button from "@/components/ui/Button"
interface CoverSettingsModalProps {
open: boolean
onClose: () => void
templates: CoverTemplate[]
selectedTemplateId: string
onSelectTemplate: (id: string) => void
onEditTemplate: (template: CoverTemplate) => void
onCreateNew: () => void
}
const GRADIENT_MAP: Record<string, string> = {
default: "linear-gradient(135deg, #e0e0e0, #c0c0c0)",
"bold-red": "linear-gradient(135deg, #ef4444, #b91c1c)",
"elegant-black": "linear-gradient(135deg, #374151, #111827)",
"gradient-blue": "linear-gradient(135deg, #3b82f6, #1d4ed8)",
"gradient-purple": "linear-gradient(135deg, #8b5cf6, #6d28d9)",
"warm-orange": "linear-gradient(135deg, #f97316, #ea580c)",
"fresh-green": "linear-gradient(135deg, #22c55e, #15803d)",
"tech-blue": "linear-gradient(135deg, #06b6d4, #0e7490)",
}
const CoverSettingsModal: React.FC<CoverSettingsModalProps> = ({
open,
onClose,
templates,
selectedTemplateId,
onSelectTemplate,
onEditTemplate,
onCreateNew,
}) => {
return (
<Modal open={open} onCancel={onClose} width={800} title="封面设置" centered footer={null}>
<div className="xx-cover-modal-toolbar">
<Button buttonType="primary">选择素材文件</Button>
<Button buttonType="ghost">导出全部</Button>
<Button buttonType="primary" onClick={onCreateNew}>
创建新模板
</Button>
</div>
<div className="xx-cover-template-grid">
{templates.map((tpl) => (
<div
key={tpl.id}
className={`xx-cover-template-card${selectedTemplateId === tpl.id ? " selected" : ""}`}
onClick={() => onSelectTemplate(tpl.id)}
>
<div
className="xx-cover-template-thumb"
style={{ background: GRADIENT_MAP[tpl.id] || GRADIENT_MAP.default }}
>
🖼️
</div>
<div className="xx-cover-template-info">
<div className="xx-cover-template-name">
{tpl.name}
{tpl.is_system && <span className="xx-cover-template-badge">✨ 系统模板</span>}
</div>
<div className="xx-cover-template-date">{tpl.created_at}</div>
<div className="xx-cover-template-actions" onClick={(e) => e.stopPropagation()}>
<Button buttonType="ghost" buttonSize="sm" onClick={() => onEditTemplate(tpl)}>
编辑
</Button>
<Button buttonType="ghost" buttonSize="sm">
导出
</Button>
</div>
</div>
</div>
))}
</div>
</Modal>
)
}
export default CoverSettingsModal
+38 -1
View File
@@ -2,7 +2,7 @@
* 智能剪辑页面 — 常量定义
*/
import type { CoverConfig } from "../editing-planner/types"
import type { CoverConfig, CoverTemplate } from "../editing-planner/types"
/* ── 克隆声音状态配置 ── */
export const CLONE_STATUS_CONFIG: Record<string, { label: string; color: string }> = {
@@ -198,3 +198,40 @@ export const DEFAULT_COVER_SETTINGS: CoverConfig = {
ai_suggested_time: null,
thumbnail_url: "",
}
/* ── Mock 封面模板数据 ── */
export const MOCK_COVER_TEMPLATES: CoverTemplate[] = [
{ id: "default", name: "默认模板", thumbnail_url: "", is_system: true, created_at: "2026-08-01" },
{ id: "bold-red", name: "大胆红", thumbnail_url: "", is_system: true, created_at: "2026-08-01" },
{
id: "elegant-black",
name: "优雅黑",
thumbnail_url: "",
is_system: true,
created_at: "2026-08-01",
},
{
id: "gradient-blue",
name: "渐变蓝",
thumbnail_url: "",
is_system: true,
created_at: "2026-08-01",
},
{
id: "gradient-purple",
name: "渐变紫",
thumbnail_url: "",
is_system: true,
created_at: "2026-08-01",
},
{ id: "warm-orange", name: "暖橙", thumbnail_url: "", is_system: true, created_at: "2026-08-01" },
{
id: "fresh-green",
name: "清新绿",
thumbnail_url: "",
is_system: true,
created_at: "2026-08-01",
},
{ id: "tech-blue", name: "科技蓝", thumbnail_url: "", is_system: true, created_at: "2026-08-01" },
]
+193 -1
View File
@@ -2383,7 +2383,6 @@
font-size: 14px;
}
/* ── 加载中状态 ── */
.xx-preview-loading {
text-align: center;
@@ -2889,3 +2888,196 @@
.ant-modal-close {
color: #fff !important;
}
/* ── 封面设置区域改造样式 ── */
/* 封面操作按钮区 */
.xx-cover-actions {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
/* 已选模板文字 */
.xx-cover-selected-template {
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 16px;
}
/* 封面设置弹窗 - 工具栏 */
.xx-cover-modal-toolbar {
display: flex;
gap: 8px;
margin-bottom: 20px;
flex-wrap: wrap;
}
/* 封面模板网格 */
.xx-cover-template-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
/* 封面模板卡片 */
.xx-cover-template-card {
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
overflow: hidden;
cursor: pointer;
transition: border-color 0.2s;
}
.xx-cover-template-card:hover {
border-color: var(--primary-color);
}
.xx-cover-template-card.selected {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
/* 卡片缩略图 */
.xx-cover-template-thumb {
aspect-ratio: 9/16;
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #ccc;
}
/* 卡片信息区 */
.xx-cover-template-info {
padding: 8px;
}
.xx-cover-template-name {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
margin-bottom: 2px;
}
.xx-cover-template-badge {
font-size: 11px;
color: #7c3aed;
background: rgba(124, 58, 237, 0.1);
padding: 1px 6px;
border-radius: 4px;
white-space: nowrap;
}
.xx-cover-template-date {
font-size: 11px;
color: var(--text-tertiary);
margin-bottom: 6px;
}
.xx-cover-template-actions {
display: flex;
gap: 4px;
}
/* 封面编辑器 */
.xx-cover-editor-layout {
display: flex;
gap: 20px;
min-height: 500px;
}
.xx-cover-editor-left {
width: 280px;
flex-shrink: 0;
}
.xx-cover-editor-right {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: var(--radius-md);
}
.xx-cover-editor-canvas {
width: 225px;
height: 400px;
background: #ddd;
position: relative;
border-radius: 8px;
overflow: hidden;
}
.xx-cover-editor-portrait {
position: absolute;
top: 20%;
left: 15%;
width: 70%;
height: 45%;
background: #a8d4f0;
border: 2px solid #333;
}
.xx-cover-editor-handle {
position: absolute;
width: 8px;
height: 8px;
background: #333;
border: 1px solid white;
}
/* 编辑器折叠面板 */
.xx-cover-editor-section {
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
margin-bottom: 8px;
}
.xx-cover-editor-section-header {
padding: 10px 12px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-tertiary);
}
.xx-cover-editor-section-body {
padding: 12px;
font-size: 12px;
color: var(--text-secondary);
}
/* 编辑器顶部 */
.xx-cover-editor-header {
margin-bottom: 16px;
}
.xx-cover-editor-name-input {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 14px;
margin-bottom: 12px;
}
.xx-cover-editor-header-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 编辑器画布内文字占位 */
.xx-cover-editor-title-placeholder {
position: absolute;
top: 72%;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
font-weight: 700;
color: #fff;
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6);
white-space: nowrap;
}
.xx-cover-editor-subtitle-placeholder {
position: absolute;
top: 82%;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: rgba(255, 255, 255, 0.85);
white-space: nowrap;
}
+2
View File
@@ -19,6 +19,8 @@ export interface UseGenerateVideoProps {
autoSubtitles: boolean
bgm: boolean
generateCount: number
/** 预览任务的 task_id(用于新确认生成 API) */
previewTaskId?: string
}
/** 生成阶段 */
+20
View File
@@ -6,6 +6,7 @@ import { useState, useCallback } from "react"
import { message } from "antd"
import type { GeneratedVideo } from "@/api/template-editor"
import { generateEditPlan, updateEditPlan, getEditPlan } from "@/api/template-editor"
import { confirmGeneration } from "@/api/generation"
import type { UseGenerateVideoProps } from "./generate-video/types"
import { getGenerationPhase } from "./generate-video/phase"
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
@@ -55,6 +56,25 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
clearTimer()
try {
// 新流程:使用确认生成 API(基于预览任务)
if (props.previewTaskId) {
// 解析分辨率
const [widthStr, heightStr] = (props.videoRatio || "1080x1920").split("x")
const outputWidth = parseInt(widthStr, 10) || 1080
const outputHeight = parseInt(heightStr, 10) || 1920
await confirmGeneration(props.previewTaskId, {
output_width: outputWidth,
output_height: outputHeight,
cover_url: props.coverSettings.upload_url || "",
custom_title: props.titleSettings.title || "",
})
startPolling()
return
}
// 旧流程:使用 EditPlan API(向后兼容)
const payload = buildEditPlanPayload(props)
// 获取或创建草稿
@@ -35,6 +35,8 @@ interface UseStep4PreviewProps {
videoRatio: string
/** 配音 voice_ids(传给后端,让预览包含配音音频) */
voiceIds?: string[]
/** 配音素材库ID(用户选择的上传音频或AI配音素材) */
voiceLibraryId?: string
/** 要生成的预览数量 */
previewCount?: number
}
@@ -84,6 +86,7 @@ export function useStep4Preview({
duration,
videoRatio,
voiceIds,
voiceLibraryId,
previewCount = 1,
}: UseStep4PreviewProps) {
const templateName = useMemo(
@@ -332,6 +335,7 @@ export function useStep4Preview({
duration: duration || undefined,
video_ratio: videoRatio,
voice_ids: voiceIds && voiceIds.length > 0 ? voiceIds : undefined,
voice_library_id: voiceLibraryId || undefined,
})
if (startTimeRef.current === 0) return
@@ -355,6 +359,7 @@ export function useStep4Preview({
duration,
videoRatio,
voiceIds,
voiceLibraryId,
previewCount,
clearPollTimer,
pollPreviewStatus,
@@ -2,9 +2,9 @@
* Step 6 封面设置 Hook
* 封装封面设置的交互逻辑
*/
import { useCallback, useRef } from "react"
import type { CoverConfig } from "../../editing-planner/types"
import { COVER_MODE_LABELS, COVER_MODE_ICONS, DEFAULT_COVER_SETTINGS } from "../constants"
import { useCallback, useRef, useState } from "react"
import type { CoverConfig, CoverTemplate } from "../../editing-planner/types"
import { MOCK_COVER_TEMPLATES } from "../constants"
import { generateCover } from "@/api/template-editor"
interface UseStep6CoverProps {
@@ -26,50 +26,12 @@ export function useStep6Cover({
}: UseStep6CoverProps) {
const generatingRef = useRef(false)
const formatTime = useCallback((seconds: number) => {
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 10)
return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}.${ms}`
}, [])
const toggleEnabled = useCallback(
(enabled: boolean) => {
onCoverSettingsChange({ ...coverSettings, enabled })
},
[coverSettings, onCoverSettingsChange],
)
const setMode = useCallback(
(mode: CoverConfig["mode"]) => {
onCoverSettingsChange({ ...coverSettings, mode })
},
[coverSettings, onCoverSettingsChange],
)
const setFrameTime = useCallback(
(frameTime: number) => {
onCoverSettingsChange({ ...coverSettings, frame_time: frameTime })
},
[coverSettings, onCoverSettingsChange],
)
const handleUpload = useCallback(
(file: File) => {
const reader = new FileReader()
reader.onload = (ev) => {
const url = ev.target?.result as string
onCoverSettingsChange({
...coverSettings,
upload_url: url,
thumbnail_url: url,
mode: "upload",
})
}
reader.readAsDataURL(file)
},
[coverSettings, onCoverSettingsChange],
)
// ── 封面设置弹窗状态 ──
const [showCoverSettings, setShowCoverSettings] = useState(false)
const [showCoverEditor, setShowCoverEditor] = useState(false)
const [selectedTemplateId, setSelectedTemplateId] = useState("default")
const [editingTemplate, setEditingTemplate] = useState<CoverTemplate | null>(null)
const [coverTemplates, setCoverTemplates] = useState<CoverTemplate[]>(MOCK_COVER_TEMPLATES)
/** 调用后端智能封面 API,生成封面并更新预览 */
const generateAutoCover = useCallback(async () => {
@@ -95,20 +57,42 @@ export function useStep6Cover({
}
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange])
// ── 模板操作方法 ──
const handleSelectTemplate = useCallback((id: string) => {
setSelectedTemplateId(id)
}, [])
const handleEditTemplate = useCallback((tpl: CoverTemplate) => {
setEditingTemplate(tpl)
setShowCoverEditor(true)
}, [])
const handleSaveTemplate = useCallback((tpl: CoverTemplate) => {
setCoverTemplates((prev) => prev.map((t) => (t.id === tpl.id ? tpl : t)))
setShowCoverEditor(false)
}, [])
const selectedTemplateName =
coverTemplates.find((t) => t.id === selectedTemplateId)?.name || "默认"
const totalDuration = duration || 30
return {
coverSettings,
formatTime,
toggleEnabled,
setMode,
setFrameTime,
handleUpload,
generateAutoCover,
totalDuration,
COVER_MODE_LABELS,
COVER_MODE_ICONS,
DEFAULT_COVER_SETTINGS,
showCoverSettings,
setShowCoverSettings,
showCoverEditor,
setShowCoverEditor,
selectedTemplateId,
setSelectedTemplateId,
editingTemplate,
coverTemplates,
selectedTemplateName,
handleSelectTemplate,
handleEditTemplate,
handleSaveTemplate,
}
}
+21 -3
View File
@@ -1058,6 +1058,7 @@ def _download_all_assets(
task_asset_ids: list[str],
voice_library_id: str,
task_id: str,
voice_ids: list[str] | None = None,
) -> tuple[list[Path], str | None]:
"""下载视频素材和配音素材。
@@ -1066,6 +1067,9 @@ def _download_all_assets(
Note: gen_task 不传入下载函数(session 已关闭),
主函数在下载前后已有汇总日志。
配音下载逻辑:优先使用 voice_library_id(配音素材库资产);
若为空则 fallback 到 voice_ids[0](前端选择的音频 asset_id)。
"""
logger.info("[task_id=%s] [下载素材] 开始下载视频素材", task_id)
download_start = time.monotonic()
@@ -1085,11 +1089,24 @@ def _download_all_assets(
)
audio_path: str | None = None
if voice_library_id:
# 配音下载:优先 voice_library_id,fallback 到 voice_ids[0]
effective_voice_id = voice_library_id
if not effective_voice_id and voice_ids:
effective_voice_id = voice_ids[0]
logger.info(
"[task_id=%s] [下载配音] voice_library_id 为空,fallback 到 voice_ids[0]=%s",
task_id,
effective_voice_id,
)
if effective_voice_id:
local_audio = temp_path / "voice.mp3"
if _download_voice_asset(voice_library_id, local_audio):
if _download_voice_asset(effective_voice_id, local_audio):
audio_path = str(local_audio)
logger.info("[task_id=%s] [下载配音] 配音下载成功", task_id)
logger.info(
"[task_id=%s] [下载配音] 配音下载成功 (source=%s)",
task_id,
"voice_library_id" if voice_library_id else "voice_ids",
)
return downloaded_videos, audio_path
@@ -1422,6 +1439,7 @@ def generate_video(self, task_id: str) -> dict:
task_asset_ids=task_asset_ids,
voice_library_id=voice_library_id,
task_id=task_id,
voice_ids=task_info.get("voice_ids", []),
)
if gen_task:
@@ -0,0 +1,151 @@
"""Tests for voice_ids fallback in _download_all_assets.
When voice_library_id is empty but voice_ids is non-empty, the Worker
should fallback to voice_ids[0] as the audio asset_id.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
class TestDownloadAllAssetsVoiceIdsFallback:
"""_download_all_assets 配音下载 fallback 逻辑测试。"""
@patch("worker_app.tasks.generation._download_voice_asset")
@patch("worker_app.tasks.generation._download_library_assets")
def test_voice_library_id_takes_priority(self, mock_download_videos, mock_download_voice, tmp_path):
"""voice_library_id 存在时优先使用,不 fallback 到 voice_ids。"""
from worker_app.tasks.generation import _download_all_assets
mock_download_videos.return_value = [tmp_path / "v1.mp4"]
mock_download_voice.return_value = True
videos, audio = _download_all_assets(
temp_path=tmp_path,
asset_library_id="lib-1",
project_id="proj-1",
task_asset_ids=["a1"],
voice_library_id="voice-lib-123",
task_id="task-1",
voice_ids=["voice-ids-456"],
)
assert audio is not None
mock_download_voice.assert_called_once()
call_args = mock_download_voice.call_args
assert call_args[0][0] == "voice-lib-123" # first positional arg
@patch("worker_app.tasks.generation._download_voice_asset")
@patch("worker_app.tasks.generation._download_library_assets")
def test_fallback_to_voice_ids_when_voice_library_id_empty(
self, mock_download_videos, mock_download_voice, tmp_path
):
"""voice_library_id 为空时 fallback 到 voice_ids[0]。"""
from worker_app.tasks.generation import _download_all_assets
mock_download_videos.return_value = [tmp_path / "v1.mp4"]
mock_download_voice.return_value = True
videos, audio = _download_all_assets(
temp_path=tmp_path,
asset_library_id="lib-1",
project_id="proj-1",
task_asset_ids=["a1"],
voice_library_id="", # 空字符串
task_id="task-2",
voice_ids=["voice-asset-789"],
)
assert audio is not None
mock_download_voice.assert_called_once()
call_args = mock_download_voice.call_args
assert call_args[0][0] == "voice-asset-789"
@patch("worker_app.tasks.generation._download_voice_asset")
@patch("worker_app.tasks.generation._download_library_assets")
def test_no_audio_when_both_empty(self, mock_download_videos, mock_download_voice, tmp_path):
"""voice_library_id 和 voice_ids 都为空时,不下载音频。"""
from worker_app.tasks.generation import _download_all_assets
mock_download_videos.return_value = [tmp_path / "v1.mp4"]
videos, audio = _download_all_assets(
temp_path=tmp_path,
asset_library_id="lib-1",
project_id="proj-1",
task_asset_ids=["a1"],
voice_library_id="",
task_id="task-3",
voice_ids=[],
)
assert audio is None
mock_download_voice.assert_not_called()
@patch("worker_app.tasks.generation._download_voice_asset")
@patch("worker_app.tasks.generation._download_library_assets")
def test_no_audio_when_voice_ids_none(self, mock_download_videos, mock_download_voice, tmp_path):
"""voice_ids 为 None 时,不触发 fallback。"""
from worker_app.tasks.generation import _download_all_assets
mock_download_videos.return_value = [tmp_path / "v1.mp4"]
videos, audio = _download_all_assets(
temp_path=tmp_path,
asset_library_id="lib-1",
project_id="proj-1",
task_asset_ids=["a1"],
voice_library_id="",
task_id="task-4",
voice_ids=None,
)
assert audio is None
mock_download_voice.assert_not_called()
@patch("worker_app.tasks.generation._download_voice_asset")
@patch("worker_app.tasks.generation._download_library_assets")
def test_voice_library_id_empty_string_fallback(self, mock_download_videos, mock_download_voice, tmp_path):
"""voice_library_id 为空字符串且 voice_ids 有多个元素时,取第一个。"""
from worker_app.tasks.generation import _download_all_assets
mock_download_videos.return_value = [tmp_path / "v1.mp4"]
mock_download_voice.return_value = True
videos, audio = _download_all_assets(
temp_path=tmp_path,
asset_library_id="lib-1",
project_id="proj-1",
task_asset_ids=["a1"],
voice_library_id="",
task_id="task-5",
voice_ids=["first-id", "second-id", "third-id"],
)
assert audio is not None
call_args = mock_download_voice.call_args
assert call_args[0][0] == "first-id"
@patch("worker_app.tasks.generation._download_voice_asset")
@patch("worker_app.tasks.generation._download_library_assets")
def test_backward_compat_no_voice_ids_param(self, mock_download_videos, mock_download_voice, tmp_path):
"""不传 voice_ids 参数时,行为与之前一致(向后兼容)。"""
from worker_app.tasks.generation import _download_all_assets
mock_download_videos.return_value = [tmp_path / "v1.mp4"]
mock_download_voice.return_value = True
# 不传 voice_ids
videos, audio = _download_all_assets(
temp_path=tmp_path,
asset_library_id="lib-1",
project_id="proj-1",
task_asset_ids=["a1"],
voice_library_id="voice-lib-999",
task_id="task-6",
)
assert audio is not None
mock_download_voice.assert_called_once_with("voice-lib-999", tmp_path / "voice.mp3")
@@ -0,0 +1,74 @@
"""预览生成 voice_library_id 透传修复测试.
Bug: 预览生成接口硬编码 voice_library_id="",导致用户选择的上传音频
在预览渲染时从未下载和混入,预览视频无声。
Fix: CreatePreviewGenerationTaskRequest 增加 voice_library_id 字段,
预览端点透传 request.voice_library_id。
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
class TestPreviewVoiceLibraryIdSchema:
"""CreatePreviewGenerationTaskRequest voice_library_id 字段测试."""
def test_default_empty_string(self):
"""不传 voice_library_id 时默认为空字符串."""
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
req = CreatePreviewGenerationTaskRequest(
template_id="tmpl_1",
asset_ids=["a1"],
)
assert req.voice_library_id == ""
def test_accepts_voice_library_id(self):
"""传入 voice_library_id 正常接收."""
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
req = CreatePreviewGenerationTaskRequest(
template_id="tmpl_1",
asset_ids=["a1"],
voice_library_id="asset_abc123",
)
assert req.voice_library_id == "asset_abc123"
def test_accepts_empty_voice_library_id(self):
"""显式传空字符串也正常."""
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
req = CreatePreviewGenerationTaskRequest(
template_id="tmpl_1",
asset_ids=["a1"],
voice_library_id="",
)
assert req.voice_library_id == ""
def test_all_fields_including_voice_library_id(self):
"""包含 voice_library_id 的完整请求."""
from app.schemas.generation_task import CreatePreviewGenerationTaskRequest
req = CreatePreviewGenerationTaskRequest(
template_id="tmpl_123",
asset_ids=["a1", "a2"],
title_ids=["t1"],
voice_ids=["v1"],
voice_library_id="voice_asset_456",
video_title="测试预览",
duration=30.0,
video_ratio="9:16",
bgm_config={"enabled": True, "volume": 0.5},
)
assert req.voice_library_id == "voice_asset_456"
assert req.asset_ids == ["a1", "a2"]
assert req.bgm_config["enabled"] is True