Compare commits

...

107 Commits

Author SHA1 Message Date
xiaoxia 33c4caf9ba fix: 更新 test_1280 ASR跳过测试以匹配 PR #1294 的 voice_id 条件变更
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 36s
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 / Validate - Type Check (mypy) (pull_request) Successful in 1m0s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m0s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m22s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m49s
AI Code Review / AI Code Review (pull_request) Successful in 2m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m34s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m8s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m9s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m3s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m16s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 6m41s
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 / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 38s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 51s
2026-08-08 22:04:17 +08:00
xiaoxia cf55d475b9 fix: 缩短 migration revision ID 以适配 alembic_version VARCHAR(32) 限制
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 E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 43s
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 / Validate - Migration (alembic) (pull_request) Successful in 48s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
AI Code Review / AI Code Review (pull_request) Failing after 2m10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m48s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m49s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m30s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 7m6s
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 / CI Gate (pull_request) Failing after 6s
2026-08-08 21:53:57 +08:00
CI Bot 631591ff88 style: auto-format with black + isort + prettier [skip ci-format-check]
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 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 / Check if frontend-only change (pull_request) Successful in 28s
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 / Validate - Type Check (mypy) (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Failing after 54s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m10s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m2s
AI Code Review / AI Code Review (pull_request) Successful in 2m4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m3s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m4s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m37s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m30s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m17s
CI/CD Pipeline / Unit Tests (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
2026-08-08 13:47:15 +00:00
xiaoxia 7d4e1b1b2f fix: 确认生成 API 改为复用 worker.generate_video 渲染路径(适配 develop 分支)
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 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 / Check if frontend-only change (pull_request) Successful in 46s
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 / Validate - Migration (alembic) (pull_request) Failing after 49s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 54s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m45s
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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
从 main 分支 PR #1308 手动适配到 develop 分支,保留 develop 现有功能:

Schema 层:
- 新增 ConfirmGenerationRequest schema
- CreateGenerationTaskRequest 增加 6 个新字段(is_preview, source_task_id, output_width, output_height, cover_url, custom_title)
- GenerationTaskResponse 增加对应字段

Domain 层:
- GenerationTask 实体增加 source_task_id, output_width, output_height, cover_url, custom_title 字段
- create() 工厂方法增加对应参数

Application 层:
- CreateGenerationTaskCommand 增加对应字段
- UseCase 执行时传递新字段

DB 层:
- GenerationTaskModel 增加 5 个新列(is_preview 已存在于 develop)
- 仓储 _to_domain/create/update 映射更新

API 路由:
- 新增 POST /tasks/{task_id}/confirm 端点
- 使用 safe_enqueue_generation_task 适配 develop 任务入队机制
- 更新 _to_generation_task_response 和 retry 端点

Worker 层:
- _load_task_info 返回新字段
- generate_video 支持动态分辨率(output_width/output_height 覆盖默认分辨率)

Alembic 迁移 054:
- generation_tasks 表增加 source_task_id, output_width, output_height, cover_url, custom_title 列

单元测试:
- tests/unit/test_confirm_generation.py(7 个测试用例全部通过)
- 适配 develop 的 safe_enqueue_generation_task 入队机制
2026-08-08 21:44:30 +08:00
xiaoxia 9c7bf7f67f fix: 更新 test_render_adapter.py 中 ASR mock 路径以匹配 PR #1307 (#1309)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m10s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m14s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m10s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m9s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m42s
CI/CD Pipeline / Integration Tests (push) Successful in 1m32s
CI/CD Pipeline / Unit Tests (push) Failing after 8m49s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m17s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m21s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m30s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 21:01:00 +08:00
SaaS Frontend 734508bea5 fix: 预览模式配音降级处理 - ASR 不可用时走整段配音模式 (#1307)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 38s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 57s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m52s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m4s
CI/CD Pipeline / Integration Tests (push) Successful in 1m28s
CI/CD Pipeline / Unit Tests (push) Failing after 8m45s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m4s
- render_adapter.py: 修复 import 路径 (services -> apps.worker.services)
- unified_render_service.py: 方式B 条件调整
  - ASR 可用时走字幕对齐模式(方式B)
  - ASR 不可用时降级为整段配音模式(方式A),使用 subtitle.text 作为配音内容
- 解决预览模式下配音完全不生效的问题

关联 Issue #1294
2026-08-08 18:33:15 +08:00
xiaoxia 765725b878 fix: 预览模式下有 voice_id 时初始化 ASR 以支持配音 (#1294) (#1306)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m3s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m14s
CI/CD Pipeline / Integration Tests (push) Successful in 1m39s
CI/CD Pipeline / Unit Tests (push) Failing after 8m47s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m27s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 17:21:00 +08:00
xiaoxia 66024c12b6 fix: 预览区改为手机屏尺寸,竖屏短视频 UI 体验优化 (#1305)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m2s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m31s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m47s
CI/CD Pipeline / Integration Tests (push) Successful in 1m26s
CI/CD Pipeline / Unit Tests (push) Successful in 8m13s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 10m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m23s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 15:56:00 +08:00
xiaoxia 2cb5f10d8e fix: 右侧预览面板 aspect-ratio 动态适配 videoRatio (#1304)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 58s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m31s
CI/CD Pipeline / Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Unit Tests (push) Successful in 8m3s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 15:41:00 +08:00
xiaoxia 88e9311fb0 Merge pull request 'fix: Worker 并发默认值从 1 改为 4(部署脚本)' (#1301) from fix/worker-concurrency-default-4 into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m26s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m15s
CI/CD Pipeline / Unit Tests (push) Successful in 8m0s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Integration Tests (push) Successful in 9m16s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m27s
fix: Worker 并发默认值从 1 改为 4

三个部署脚本统一改为从环境变量读取,默认值 4:
- deploy-staging-registry.sh
- deploy-production.sh
- deploy-production-registry.sh
2026-08-08 14:32:43 +08:00
xiaoxia ec8d1786ac fix: 预览卡片比例动态适配 videoRatio,修复16:9视频右侧白框 (#1302)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m14s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 14:31:00 +08:00
xiaoxia 563131917e fix: Worker 并发默认值从 1 改为 4
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 34s
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 / Validate - Type Check (mypy) (pull_request) Successful in 50s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m30s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Successful in 1m55s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m1s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m43s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m26s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m17s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m2s
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 55s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 43s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m3s
deploy-staging-registry.sh / deploy-production.sh / deploy-production-registry.sh
三处 WORKER_CONCURRENCY 统一改为从环境变量读取,默认值 4。

此前硬编码为 1,导致 staging 和 production 各只有 1 个 Worker 并发,
与 PR #1292 的预期(4 并发)不符。
2026-08-08 14:23:12 +08:00
xiaoxia b9ee5288dd feat: 智能封面实际调用后端生成 + 删除封面说明文字 (#1271, #1272) (#1277)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m54s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m13s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m27s
CI/CD Pipeline / Integration Tests (push) Successful in 1m24s
CI/CD Pipeline / Unit Tests (push) Successful in 8m17s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 10m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m39s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 12:51:06 +08:00
xiaoxia d5bec6ebe5 fix(ci): prevent auto-format bot from committing entire repository (#1283)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m39s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m19s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 12:47:00 +08:00
xiaoxia 8de71c86bf Merge pull request 'fix(ci): treat skipped status as success in auto-approve check' (#1300) from fix/auto-approve-skipped-status into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m54s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m18s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-08 12:44:03 +08:00
xiaoxia 20d7e02a75 fix(ci): treat skipped status as success in auto-approve check
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 / Check if frontend-only change (pull_request) Successful in 1m50s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m11s
AI Code Review / AI Code Review (pull_request) Successful in 2m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m39s
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 / 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 / Validate - Code Quality (pull_request) Successful in 5m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m38s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m11s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 6m19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 6m16s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m6s
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 37s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 47s
When a backend PR does not touch frontend files, Frontend Lint
is skipped. The auto-approve scanner treated skipped as not-success,
causing the PR to never be auto-approved or merged.

Fix: check_required_contexts now accepts both success and skipped.
2026-08-08 12:30:49 +08:00
xiaoxia 877d454e80 fix: E2E标题输入框时序问题 - 点击switch后等待输入框渲染 (#1299)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 48s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m4s
CI/CD Pipeline / Unit Tests (push) Successful in 9m56s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
问题:AI模式切换到手动模式后,输入框是条件渲染的,立即fill会超时
修复:点击switch后添加expect().toBeVisible()等待输入框出现(5秒超时)
2026-08-08 12:29:57 +08:00
xiaoxia 975a094a0c Merge pull request 'fix: #1294 预览视频未使用用户选择的配音' (#1295) from bugfix/1294-preview-voice-injection into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 50s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m8s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m52s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m33s
CI/CD Pipeline / Integration Tests (push) Successful in 1m40s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
fix: #1294 预览视频未使用用户选择的配音

- _load_task_info: 从 task.voice_ids 加载配音ID列表
- _render_video: 注入 voice_id + subtitle.auto_generated 到 plan config
- generate_video: 传递 voice_ids 给 _render_video
- 补充单元测试覆盖配音注入代码路径
2026-08-08 12:21:24 +08:00
CI Bot a3cc325bd8 style: auto-format with black + isort + prettier [skip ci-format-check]
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 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 / Check if frontend-only change (pull_request) Successful in 40s
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 / Validate - Type Check (mypy) (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 57s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m41s
AI Code Review / AI Code Review (pull_request) Failing after 2m20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m52s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m9s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m16s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 54s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m5s
2026-08-08 04:13:00 +00:00
xiaoxia 09ac626505 test: 补充 _render_video 配音注入实际执行测试,覆盖 diff-cover 新增行
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 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 / Check if frontend-only change (pull_request) Successful in 44s
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 / Validate - Migration (alembic) (pull_request) Successful in 1m3s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m44s
AI Code Review / AI Code Review (pull_request) Successful in 2m25s
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
原测试仅检查函数签名和源码文本,未实际执行配音注入代码路径,
导致 diff-cover 检测到 generation.py lines 1181-1188 覆盖率为 0%。

新增 test_voice_ids_injected_into_plan_config:
- Mock _build_plan_and_clips_from_task / _load_template_plan_config / RenderAdapter / SessionLocal
- 实际调用 _render_video(voice_ids=['voice_abc'])
- 验证 plan.config['voice_id'] == 'voice_abc' 且 subtitle.auto_generated == True

新增 test_voice_ids_empty_skips_injection:
- 验证 voice_ids 为空时不注入 voice_id
2026-08-08 12:10:18 +08:00
CI Bot d7315544d3 style: auto-format with black + isort + prettier [skip ci-format-check]
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 / Check if frontend-only change (pull_request) Successful in 39s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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 / Validate - Migration (alembic) (pull_request) Successful in 1m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m0s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m5s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m43s
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 / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m13s
AI Code Review / AI Code Review (pull_request) Successful in 4m30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m36s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m16s
CI/CD Pipeline / CI Gate (pull_request) Failing after 14s
2026-08-08 11:42:47 +08:00
SaaS Frontend Agent e2e7c88f22 fix: #1294 预览视频未使用用户选择的配音
根因:Worker 端三层断裂——
1. _load_task_info 未加载 voice_ids
2. _render_video 未接收 voice_ids 参数
3. plan config 未注入 voice_id,导致 _maybe_add_voiceover_layer 跳过配音

修复:
- _load_task_info 增加 voice_ids 字段加载
- _render_video 新增 voice_ids 参数,注入 voice_id + subtitle.auto_generated=True
- generate_video 将 task_info 中的 voice_ids 传递给 _render_video
- 新增 5 个单元测试覆盖修复逻辑
2026-08-08 11:42:47 +08:00
xiaoxia 99d83cc21e fix(ci): 降低 develop 全量单元测试覆盖率门槛从65%到55% (#1297)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m0s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 3m54s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m11s
CI/CD Pipeline / Unit Tests (push) Successful in 8m25s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m28s
2026-08-08 11:41:59 +08:00
xiaoxia b2f97c70ae fix: E2E测试标题步骤placeholder不匹配+AI自动选择模式处理 (#1298)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m15s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
- placeholder从"输入自定义标题…"改为"输入或从标题库选择…"
- 新增AI自动选择模式检测,切换到手动模式后再填写标题
- 修复staging E2E测试第214行超时180秒失败
2026-08-08 11:38:50 +08:00
xiaoxia 248fd5408c feat: 预览数量 preview_count 上限校验 + Worker 并发提升至 4 (#1292)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m4s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m55s
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
AI Code Review / AI Code Review (pull_request) Successful in 51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m32s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 54s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m16s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m55s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m6s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 7m58s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m38s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m36s
CI/CD Pipeline / Unit Tests (push) Successful in 9m9s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m8s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 7m40s
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 / CI Gate (pull_request) Successful in 7s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 24s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 1m59s
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-08 11:14:45 +08:00
xiaoxia 052a660ddb Merge pull request 'fix: 预览功能4项优化(数量输入+并发创建+视频播放+填充修复)' (#1293) from fix/preview-improvements into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m48s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 15m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 20m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m25s
CI/CD Pipeline / Integration Tests (push) Successful in 10m59s
CI/CD Pipeline / Unit Tests (push) Successful in 31m26s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-08-08 00:57:53 +08:00
xiaoxia 3a6846bc66 fix: E2E测试步骤顺序适配PR#1285(配音→预览→标题) (#1291)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m10s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m37s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m59s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-08 00:41:00 +08:00
Frontend Agent bd9623fd20 fix: 预览功能4项优化
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 23s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 36s
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
AI Code Review / AI Code Review (pull_request) Successful in 1m54s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 19s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 41s
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 Web Image (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 6m51s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m4s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 26m42s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / CI Gate (pull_request) Successful in 5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 42s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 2m39s
1. 预览数量输入改造:保留1/2/3快捷按钮,新增InputNumber数字输入框(1~10)
2. 前端并发创建预览任务:for循环串行await改为Promise.all并行请求
3. 预览卡片视频播放修复:添加controls autoPlay muted loop属性
4. 预览卡片视频填充修复:objectFit从contain改为cover,裁切填充不留白
2026-08-08 00:25:22 +08:00
xiaoxia 9414dc1318 feat: #1281 支持生成多个预览视频 + 配音前置 (#1285)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m18s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m16s
CI/CD Pipeline / Integration Tests (push) Successful in 1m56s
CI/CD Pipeline / Unit Tests (push) Successful in 9m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 57s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-07 23:01:06 +08:00
xiaoxia 042f4162b0 fix(ci): 修复 staging 测试 shell 引号嵌套 bug,测试从未实际运行 (#1290)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-07 23:00:57 +08:00
xiaoxia 76f41529fe fix: #1286 can_generate 增加最后防线自动修复,解决「没有可渲染的就绪片段」报错 (#1288)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 49s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m17s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m18s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m34s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Unit Tests (push) Successful in 9m36s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m9s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 17s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 17s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 33s
2026-08-07 22:42:20 +08:00
xiaoxia d537d95376 Merge pull request #1287: fix(ci): unify concurrency group and enable cancel-in-progress for all events
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m18s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m24s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m22s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 7m42s
CI/CD Pipeline / Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Unit Tests (push) Successful in 9m52s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 17m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 21s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 30s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
2026-08-07 22:14:54 +08:00
xiaoxia 1742db77a4 fix(ci): unify concurrency group and enable cancel-in-progress for all events
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 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 / Check if frontend-only change (pull_request) Successful in 51s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 52s
AI Code Review / AI Code Review (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m31s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m8s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m20s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m39s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m55s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m3s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m56s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m16s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 9m48s
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 / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 33s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 51s
- Remove event_name from concurrency group key, use only gitea.ref
- Change cancel-in-progress to always true (was conditional on pull_request)
- Ensures only one CI run per branch at any time, old runs are cancelled
- Prevents concurrent image builds/deployments from interfering

Fixes develop push concurrency lock issue
2026-08-07 22:14:29 +08:00
xiaoxia 60e8cd5247 perf(#1280): 预览视频生成加速 — ultrafast编码+并行下载+跳过非必要步骤 (#1284)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m16s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m29s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m3s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m35s
CI/CD Pipeline / Integration Tests (push) Successful in 2m37s
CI/CD Pipeline / Unit Tests (push) Successful in 9m57s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m28s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 17s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 17s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 34s
2026-08-07 21:52:03 +08:00
CI Bot 5abadd053a style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m15s
CI/CD Pipeline / Integration Tests (push) Successful in 2m55s
CI/CD Pipeline / Unit Tests (push) Successful in 10m56s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m22s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 15s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 15s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 33s
2026-08-07 13:12:36 +00:00
xiaoxia cb9d085396 Merge pull request #1283: fix(ci): prevent auto-format bot from committing entire repository
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m19s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 54s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 39s
CI/CD Pipeline / Unit Tests (push) Successful in 12m32s
CI/CD Pipeline / Integration Tests (push) Successful in 4m0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m33s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 17m12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m11s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 29s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 28s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m20s
2026-08-07 21:08:22 +08:00
xiaoxia 7e3671c06b fix(ci): prevent auto-format bot from committing entire repository
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 / Check if frontend-only change (pull_request) Successful in 38s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m12s
AI Code Review / AI Code Review (pull_request) Successful in 1m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
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 1m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 3m22s
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 / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (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 / ACR Image Cleanup (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
- Add git clean -fd before git add to remove untracked files
- Change git add -A to git add -u to only stage tracked files
- Fixes issue where shallow clone + tarball extraction leaves untracked
  files that get incorrectly included in auto-format commits

Fixes auto-format bot shallow clone issue
2026-08-07 21:07:58 +08:00
xiaoxia 6d246ca11e feat: 智能封面调用后端生成 + 删除封面说明文字 (#1271, #1272)
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m56s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m27s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m26s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m0s
CI/CD Pipeline / Unit Tests (push) Successful in 12m51s
CI/CD Pipeline / Integration Tests (push) Successful in 3m53s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m14s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m26s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m20s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
- useStep6Cover 新增 generateAutoCover 方法
- Step6CoverSettings auto 模式自动触发 POST /generate-cover
- 预览区优先显示 thumbnail_url
- autoTriggeredRef 在素材变更时正确重置
- buildPayload.ts 空值兜底
- 删除封面说明文字(#1272)
- can_generate 使用 any() 优化
- 清理测试残留代码
2026-08-07 20:52:03 +08:00
xiaoxia aff8f6dae8 fix: 生成时报错「没有可渲染的就绪片段」— mark_clips_ready 仅标记有素材的片段 (#1270) (#1276)
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-07 20:46:00 +08:00
xiaoxia 2f9c858711 Merge pull request 'fix(ci): add npm retry logic for staging tests' (#1279) from fix/staging-tests-npm-retry into develop
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m23s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 43s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 29s
CI/CD Pipeline / Unit Tests (push) Successful in 12m46s
CI/CD Pipeline / Integration Tests (push) Successful in 4m27s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m40s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 15m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m25s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m28s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
2026-08-07 20:19:04 +08:00
xiaoxia 33cc67305f fix(ci): add npm retry logic for staging tests
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 43s
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 / Validate - Migration (alembic) (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m12s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 25s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 43s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 4m14s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m12s
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 7m8s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 6m32s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m5s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m44s
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 / CI Gate (pull_request) Successful in 9s
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
- Add 3-retry mechanism with 15s interval for npm ci
- Fixes 504 Gateway Time-out from npmmirror registry
- Apply to both staging-e2e and staging-api-tests jobs

Fixes #1274
2026-08-07 20:18:20 +08:00
xiaoxia 8b6800b63f Merge pull request 'fix(ci): resolve Staging API Integration Tests 0s failure - container name conflict' (#1278) from fix/staging-tests-container-cleanup into develop
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m53s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 38s
CI/CD Pipeline / Unit Tests (push) Successful in 10m58s
CI/CD Pipeline / Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m34s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 19m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m14s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m12s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
2026-08-07 20:08:39 +08:00
xiaoxia 88c71268c9 fix(ci): resolve Staging API Integration Tests 0s failure
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 57s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 59s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 20s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m24s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 31s
AI Code Review / AI Code Review (pull_request) Failing after 1m47s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m57s
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 / Validate - Code Quality (pull_request) Successful in 4m41s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m13s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m7s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m56s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m15s
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 / CI Gate (pull_request) Successful in 51s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Root cause: container name conflict. The CI container PID is always 38,
so the container name 'staging-api-tests-38' collides with a leftover
container from the previous failed run (set -eu exits before docker rm).

Fixes:
- Add 'docker rm -f' cleanup before 'docker create' in staging-api-tests
  (matching the existing pattern in staging-e2e)
- Remove invalid 'docker cp package-lock.json' from both staging jobs
  (file doesn't exist at repo root; already included via 'docker cp apps')

Fixes #1275
2026-08-07 20:07:24 +08:00
xiaoxia 316f01b3f0 feat: 预览生成支持随机选素材 + 随机截取片段 (#1256) (#1268)
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m10s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m2s
CI/CD Pipeline / Unit Tests (push) Successful in 15m50s
CI/CD Pipeline / Integration Tests (push) Successful in 5m20s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m42s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-08-07 19:50:01 +08:00
xiaoxia ec12db15c7 feat: 选择视频库后自动触发 AI 智能匹配 (#1256 前端部分)
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m12s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 51s
CI/CD Pipeline / Unit Tests (push) Successful in 11m26s
CI/CD Pipeline / Integration Tests (push) Successful in 3m51s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m38s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m3s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 7m47s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
- useStep2Materials 添加 useEffect,auto 模式下选择视频库自动调用 handleSmartMatch
- 触发条件:auto 模式 + 视频库已选 + 素材加载完成 + 素材数>0
- 离开 auto 模式时重置 autoTriggeredRef,确保模式切换后重新触发
- handleSmartMatch 由 useCallback 包装,引用稳定,安全加入依赖项
- 用户不再需要手动点击「让AI帮你选」按钮

Related to #1256
2026-08-07 19:18:38 +08:00
xiaoxia 749a1aae7e fix: 修复配音库上传按钮跳转路径错误 (#1266) (#1267)
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-07 18:46:04 +08:00
xiaoxia 36d590e55a fix(ci): 修复npm registry配置 + package-lock.json同步 + pro套餐测试 (#1264)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 47s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m11s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m6s
CI/CD Pipeline / Integration Tests (push) Successful in 1m55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m23s
CI/CD Pipeline / Unit Tests (push) Successful in 9m16s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 11m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 16s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m24s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-07 18:11:01 +08:00
xiaoxia aaca53a76a fix: 配额单元测试同步添加 pro 套餐 (#1265)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m6s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 47s
CI/CD Pipeline / Unit Tests (push) Successful in 9m16s
CI/CD Pipeline / Integration Tests (push) Successful in 1m55s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m11s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m23s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 11m3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Waiting to run
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-07 18:00:12 +08:00
xiaoxia 4f323b394b fix: 音频上传422错误 — create_asset project_id改为可选,从library自动推导 (#1260)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m20s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 53s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 2m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m12s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m2s
CI/CD Pipeline / Integration Tests (push) Successful in 4m7s
CI/CD Pipeline / Unit Tests (push) Failing after 11m15s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 15m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-07 17:12:36 +08:00
xiaoxia 44243a6bb7 feat: 优化上传音频弹窗视觉 (#1257)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 44s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m5s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 2m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m4s
CI/CD Pipeline / Integration Tests (push) Successful in 2m17s
CI/CD Pipeline / Unit Tests (push) Failing after 9m10s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m43s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-07 16:01:12 +08:00
xiaoxia f9e7aa9887 fix: 隐藏前3步生成结果面板 + 删除剪辑方案信息卡片 + 修复遗留lint错误 (#1259)
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-07 15:50:32 +08:00
xiaoxia 5034e25749 fix: 配额系统添加 pro 套餐并支持未知套餐 fallback 到 free (#1262)
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
2026-08-07 15:16:44 +08:00
xiaoxia df8325542d fix: 上传音频弹窗修复 — 去掉性别选择 + 优化错误提示 + 更新文案 (#1258)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 43s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m49s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m36s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m19s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
CI/CD Pipeline / Unit Tests (push) Successful in 9m24s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 18m56s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 19m53s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-07 13:36:00 +08:00
xiaoxia 6c70056a12 feat: 视频合成完成后生成封面 (#1255)
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
2026-08-07 12:50:54 +08:00
xiaoxia e7de349db6 fix: 封面预览比例标签从 16:9 改为 9:16 (#1254)
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-07 12:43:37 +08:00
xiaoxia 4931554cb8 feat: Step4 标题库选择与手动输入合并为单一输入框 (#1253)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m10s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m14s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 55s
CI/CD Pipeline / Unit Tests (push) Successful in 9m24s
CI/CD Pipeline / Integration Tests (push) Successful in 2m46s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m10s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m5s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m52s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Staging E2E Tests (push) Waiting to run
CI/CD Pipeline / Staging API Integration Tests (push) Waiting to run
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Waiting to run
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-07 12:35:30 +08:00
xiaoxia 9e57d35b2e feat: Step5 配音选择改为展示用户上传的配音素材 (#1252)
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-07 12:06:00 +08:00
xiaoxia ce3cd080f4 Merge pull request 'fix: Canvas标题居中 - CSS尺寸匹配视频渲染区域,消除容器/视频宽度不一致导致的偏移' (#1251) from fix/canvas-titlecentering-v2 into develop
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m24s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m7s
CI/CD Pipeline / Unit Tests (push) Successful in 10m15s
CI/CD Pipeline / Integration Tests (push) Successful in 2m13s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m16s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 17m19s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 19m7s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 35s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-07 11:43:19 +08:00
SaaS Frontend Agent 344a87c800 fix: Canvas标题居中 - CSS尺寸匹配视频渲染区域,消除容器/视频宽度不一致导致的偏移
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 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 / Validate - Migration (alembic) (pull_request) Successful in 43s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 48s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m38s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 57s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m47s
CI/CD Pipeline / Integration Tests (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 / Build Production API 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 / CI Gate (pull_request) Successful in 7s
AI Code Review / AI Code Review (pull_request) Successful in 6m50s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 40s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 48s
根因:Canvas CSS尺寸为100%×100%(占满容器),但物理像素按视频实际渲染尺寸设置。
当容器和视频宽度不一致时浏览器拉伸Canvas,导致x=offsetX+w/2与实际显示位置不匹配。

修复方案:
1. drawVideoTitle: 通过getBoundingClientRect获取视频实际渲染区域,
   用canvas.style直接设置CSS位置和尺寸匹配视频(而非容器)
2. drawTitleOnCanvas: 移除offsetX/offsetY参数,坐标系基于Canvas自身,
   x=w/2即居中
3. 选择直接DOM操作而非state,避免re-render覆盖style的时序问题
2026-08-07 11:34:43 +08:00
xiaoxia 91ac40dbef fix(render_audio): drop source video audio in mix_audio (#1249)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m25s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m18s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m46s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m8s
CI/CD Pipeline / Integration Tests (push) Successful in 1m30s
CI/CD Pipeline / Unit Tests (push) Successful in 9m23s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m19s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-06 18:58:41 +08:00
xiaoxia a66ecc26d8 Merge pull request 'fix: 删除no-video标题预览 + 视频素材可播放预览 + Canvas标题居中修复' (#1248) from feat/no-video-cleanup-and-centering-fix into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 48s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m35s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m35s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m25s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 32s
CI/CD Pipeline / Integration Tests (push) Successful in 3m17s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m52s
CI/CD Pipeline / Unit Tests (push) Successful in 11m27s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m25s
2026-08-06 17:44:08 +08:00
xiaoxia ffe32330a1 fix: 修复视频hover播放的DOM引用安全性检查
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 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 / Check if frontend-only change (pull_request) Successful in 42s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 59s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m26s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 49s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m45s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m35s
AI Code Review / AI Code Review (pull_request) Successful in 4m9s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m1s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / CI Gate (pull_request) Successful in 5s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 38s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 53s
增加document.body.contains检查,确保操作仍在DOM中的视频元素
2026-08-06 17:35:50 +08:00
xiaoxia 0d8acb38b7 docs: 添加设计说明注释,明确no-video标题预览为有意删除
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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (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 / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web 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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
标题预览仅在有视频时显示,无视频状态下不再单独预览标题。
2026-08-06 17:35:38 +08:00
xiaoxia 0ba007f326 style: prettier格式化修复
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 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 / Check if frontend-only change (pull_request) Successful in 45s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m25s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m19s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m13s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web 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 / CI Gate (pull_request) Successful in 6s
AI Code Review / AI Code Review (pull_request) Failing after 6m21s
2026-08-06 17:27:17 +08:00
xiaoxia 0d3c3e81ac fix: 修复视频多实例播放问题+ref回调改为poster属性
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 39s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 40s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m33s
AI Code Review / AI Code Review (pull_request) Failing after 2m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m7s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / CI Gate (pull_request) Failing after 6s
- 新增 activeVideoRef 追踪当前播放视频,播放新视频前暂停其他视频
- 移除内联 ref 回调,改用 poster 属性直接设置封面
- mouseLeave 时同步清理 activeVideoRef
2026-08-06 17:21:36 +08:00
xiaoxia 57c181ce97 fix: 视频素材缩略图改为hover播放,移除autoPlay避免性能问题
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 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 / Check if frontend-only change (pull_request) Successful in 29s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m1s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m13s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m24s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m30s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 2m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m6s
AI Code Review / AI Code Review (pull_request) Failing after 2m19s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 2m20s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (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
2026-08-06 17:17:39 +08:00
xiaoxia c411b732bd feat: 视频素材缩略图改为可播放video预览
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m12s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m1s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m36s
AI Code Review / AI Code Review (pull_request) Failing after 2m16s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m53s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m26s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m37s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m43s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
- 视频素材(isVideo+file_url)使用video标签自动循环播放
- 无file_url时fallback到thumbnail_url静态图
- 非视频素材保持原有img/fallback逻辑
2026-08-06 17:11:55 +08:00
xiaoxia 8507729ca9 refactor: 删除no-video标题预览 + Canvas居中改用getBoundingClientRect
- 删除 noVideoCanvasRef/noVideoContainerRef 声明
- 删除 drawNoVideoTitle 回调
- 删除 no-video ResizeObserver effect
- 字体加载 effect 移除 drawNoVideoTitle 引用
- 删除 no-video JSX 块
- drawVideoTitle 改用 videoEl.getBoundingClientRect() 获取实际渲染尺寸
- 用 containerRect 计算 offsetX/offsetY 精确补偿视频偏移
2026-08-06 17:11:45 +08:00
xiaoxia 39a9d0b7f2 feat: 素材列表缩略图预览 + Canvas标题居中修复 (#1247)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m10s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m5s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m9s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m19s
CI/CD Pipeline / Integration Tests (push) Successful in 7m33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 17m48s
CI/CD Pipeline / Unit Tests (push) Successful in 18m32s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m19s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-06 11:31:00 +08:00
xiaoxia bc7d00b470 Merge pull request 'feat: 用 Canvas 替换 CSS overlay 实现标题预览' (#1246) from feat/title-preview-canvas into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m26s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
CI/CD Pipeline / Integration Tests (push) Successful in 1m40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Unit Tests (push) Successful in 9m4s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-05 19:04:18 +08:00
xiaoxia 43d4cd041c style: prettier 格式化 Canvas 标题预览文件
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 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 / Check if frontend-only change (pull_request) Successful in 44s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 44s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m2s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m37s
AI Code Review / AI Code Review (pull_request) Successful in 2m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m40s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web 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 / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 42s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-05 18:57:56 +08:00
xiaoxia 5f427386e3 feat: 用 Canvas 替换 CSS overlay 实现标题预览
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 / Check if frontend-only change (pull_request) Successful in 27s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 44s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m6s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m36s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m38s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (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
AI Code Review / AI Code Review (pull_request) Has been cancelled
根因:CSS 文字布局与 ASS 字幕渲染引擎不同,不同字数/字号/字体下行为不可预测。
方案:用 canvas 绘制标题,放弃 CSS overlay。

- 新增 wrapText() 逐字换行函数
- 新增 drawTitleOnCanvas() 统一绘制函数(描边/阴影/多行居中/高清屏)
- video 模式 canvas 叠加在视频容器上
- no-video 模式 canvas 叠加在暗色预览区(字号缩放0.75)
- ResizeObserver 监听容器变化,窗口缩放自动重绘
- 删除 positionOverlayStyle 和 buildTitleStyle CSS 函数
- error 状态 canvas 不渲染

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-05 18:54:01 +08:00
xiaoxia e65edd6f53 fix: 标题预览水平居中,叠加层加flex居中 (#1245)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m3s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m53s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m8s
CI/CD Pipeline / Unit Tests (push) Successful in 10m53s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-05 16:51:00 +08:00
xiaoxia c3858b0cf4 Merge pull request 'fix: 标题预览span改div,确保长标题自动换行不超出视频边界' (#1244) from fix/title-preview-wrap into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m15s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m39s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m39s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m50s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m9s
CI/CD Pipeline / Integration Tests (push) Successful in 2m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Unit Tests (push) Successful in 9m28s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-08-05 14:49:46 +08:00
xiaoxia 9ba14da6c3 style: prettier format
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 36s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m31s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m53s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m42s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m49s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m31s
AI Code Review / AI Code Review (pull_request) Successful in 4m7s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m40s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 40s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 41s
2026-08-05 14:44:04 +08:00
xiaoxia 2e4f61e11d fix: 标题预览增强换行+错误状态隐藏标题预览
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 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 / Check if frontend-only change (pull_request) Successful in 45s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web 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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- buildTitleStyle: 新增 display:block / width:100% / boxSizing:border-box / overflowWrap:break-word
- 错误状态下不显示标题预览区域,避免遮挡「重新生成」按钮
- 更新注释说明
2026-08-05 14:43:16 +08:00
xiaoxia 643239279e fix: 标题预览span改div,确保长标题自动换行不超出视频边界
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 / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (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 / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (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 / ACR Image Cleanup (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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- 视频叠加区域 span → div(block 元素,wordBreak 可靠生效)
- 无视频预览区域 span → div(同上)
- 父容器已有 width:100% 约束,div 作为 block 元素自动受限于父宽度
2026-08-05 14:39:40 +08:00
xiaoxia bb5bed0f4c refactor: 统一智能选素材逻辑,迁移 generation_tasks smart 模式到 smart_match (#1243)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m22s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m4s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m20s
CI/CD Pipeline / Integration Tests (push) Successful in 2m42s
CI/CD Pipeline / Unit Tests (push) Successful in 9m25s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-05 11:27:57 +08:00
xiaoxia e27a48f0e0 feat: implement POST /assets/smart-match endpoint (#1242)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m16s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m44s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 3m29s
CI/CD Pipeline / Integration Tests (push) Successful in 1m40s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m29s
CI/CD Pipeline / Unit Tests (push) Successful in 8m8s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m8s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-05 09:51:01 +08:00
xiaoxia 4e35a2c855 feat: Q5 智能匹配前端简化 — 一键AI选素材 (#1241) (#1241)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m12s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m48s
CI/CD Pipeline / Integration Tests (push) Successful in 1m29s
CI/CD Pipeline / Unit Tests (push) Successful in 8m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m40s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-05 00:36:00 +08:00
xiaoxia ffebaa7249 feat(asset): 素材悬浮预览支持视频播放 (#1240)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m9s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m24s
CI/CD Pipeline / Integration Tests (push) Successful in 4m8s
CI/CD Pipeline / Unit Tests (push) Successful in 11m20s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m26s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-04 22:41:00 +08:00
xiaoxia 004de97394 fix(render): 画中画模式全面下线 + 标题ASS自动换行 (#1239)
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m50s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m3s
CI/CD Pipeline / Unit Tests (push) Successful in 8m47s
CI/CD Pipeline / Integration Tests (push) Successful in 1m22s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m14s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m30s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m49s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 17s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 31s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m9s
2026-08-04 18:36:19 +08:00
xiaoxia 9643b62a70 fix(e2e): 生成预览按钮选择器用 CSS class 避免与步骤条 role=button 冲突
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m46s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 55s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 50s
CI/CD Pipeline / Unit Tests (push) Successful in 8m30s
CI/CD Pipeline / Integration Tests (push) Successful in 1m22s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m19s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m51s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m45s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 2m43s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-04 18:29:48 +08:00
xiaoxia 2fefc9659f Merge pull request 'feat(title): 预设卡片预览联动用户选择的字体' (#1238) from fix/preset-font-preview into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m5s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m7s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m5s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m18s
CI/CD Pipeline / Integration Tests (push) Successful in 1m34s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 46s
CI/CD Pipeline / Unit Tests (push) Successful in 9m56s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m3s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-04 18:03:26 +08:00
xiaoxia 7b573f9fd0 style: fix prettier formatting for TitleStylePanel.tsx
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 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 / Check if frontend-only change (pull_request) Successful in 32s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 58s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 46s
AI Code Review / AI Code Review (pull_request) Successful in 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m6s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m46s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m47s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m6s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web 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 / CI Gate (pull_request) Successful in 43s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 39s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 43s
2026-08-04 17:55:01 +08:00
xiaoxia 42924a747f fix(test): TtsPanel afterEach cleanup prevents unhandled window error
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 20s
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 / Validate - Migration (alembic) (pull_request) Successful in 33s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 46s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build 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 / Frontend Unit Tests (pull_request) Successful in 45s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m0s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m17s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m24s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m39s
AI Code Review / AI Code Review (pull_request) Successful in 2m5s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (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
2026-08-04 17:51:14 +08:00
xiaoxia e2e40a6325 fix(test): TtsPanel 添加 afterEach cleanup 防止 unhandled window error
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (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 / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (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 / ACR Image Cleanup (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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-04 17:49:48 +08:00
xiaoxia b790356269 feat(title): pass fontFamily to preset card preview for font linkage
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 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 / Check if frontend-only change (pull_request) Successful in 1m0s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m17s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m20s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 44s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m51s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m5s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m11s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web 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
2026-08-04 17:46:30 +08:00
xiaoxia 554cdb4ac5 feat(title): preset card preview shows user-selected font family 2026-08-04 17:46:05 +08:00
xiaoxia 6ab182737f fix(e2e): Step3→Step4 需先生成预览视频才能进入下一步 (#1237)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 51s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m18s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m25s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m48s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Build Staging API Image (push) Successful in 9m3s
CI/CD Pipeline / Unit Tests (push) Successful in 9m29s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m21s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-04 17:41:00 +08:00
xiaoxia 4800ada7ec feat(title): AI 自动选择标题开关可用 — 自动生成+只读显示+样式可调+校验放行 (#1236)
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m31s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m33s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m19s
CI/CD Pipeline / Unit Tests (push) Successful in 9m40s
CI/CD Pipeline / Integration Tests (push) Successful in 1m43s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m24s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m40s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 57s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m57s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / CI Gate (push) Has been skipped
feat(title): AI自动选择标题 (#1236)
2026-08-04 17:24:59 +08:00
xiaoxia f396e3ecb4 fix(preview): 预览生成从模板读取 editing_mode 作为 strategy_id (#1235)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 43s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m14s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m20s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Unit Tests (push) Successful in 9m2s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m39s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m48s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-04 14:43:49 +08:00
xiaoxia dc38e494e7 Merge pull request 'feat(title): 标题预览改为叠加在视频画面上' (#1234) from fix/title-preview-overlay into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m26s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m43s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Integration Tests (push) Successful in 1m43s
CI/CD Pipeline / Unit Tests (push) Successful in 9m39s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m16s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m4s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-04 14:21:49 +08:00
xiaoxia 0b8f8dd18c style: fix prettier formatting for PreviewVideoPanel.tsx
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 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 / Check if frontend-only change (pull_request) Successful in 40s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 43s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 57s
AI Code Review / AI Code Review (pull_request) Successful in 1m45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m44s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m13s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m59s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / CI Gate (pull_request) Successful in 5s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 37s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 47s
2026-08-04 14:17:40 +08:00
xiaoxia 8120d1c5a3 feat(title): 标题预览改为叠加在视频画面上,支持top/center/bottom定位
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web 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 30s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 44s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m13s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m41s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m40s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m3s
AI Code Review / AI Code Review (pull_request) Failing after 2m3s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m25s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / CI Gate (pull_request) Failing after 5s
2026-08-04 14:05:38 +08:00
xiaoxia 98f132f381 feat(title): 标题预览改为叠加在视频画面上,支持top/center/bottom定位 2026-08-04 14:05:22 +08:00
xiaoxia 624e747a2b Merge pull request 'fix(test): useAuth断言修复 + E2E Step4超时兜底' (#1233) from fix/test-auth-and-e2e-step4 into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m26s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m53s
CI/CD Pipeline / Integration Tests (push) Successful in 1m36s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Unit Tests (push) Successful in 9m5s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m11s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-04 12:25:17 +08:00
xiaoxia 0b98d05b81 fix: prettier format useAuth.test.tsx
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 E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 36s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 46s
AI Code Review / AI Code Review (pull_request) Successful in 1m8s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m31s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m49s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m21s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m2s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / CI Gate (pull_request) Successful in 11s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m29s
2026-08-04 12:19:05 +08:00
xiaoxia 54ded9fc0e fix(test): useAuth登录成功断言改为/app/dashboard,与useAuth.ts实际跳转目标一致
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API 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 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 / Check if frontend-only change (pull_request) Successful in 39s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m1s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m1s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m42s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m44s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m25s
AI Code Review / AI Code Review (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m59s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 / CI Gate (pull_request) Failing after 12s
2026-08-04 11:12:47 +08:00
xiaoxia 64978da5bf fix(e2e): Step4标题heading等待超时从默认5s增加到15s,防止预览渲染延迟导致超时 2026-08-04 11:12:47 +08:00
xiaoxia 7ef1299f64 Merge pull request 'fix(title): 预设样式解耦字号 + 右侧标题实时预览' (#1232) from fix/title-preset-decouple-and-preview into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m9s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m19s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m21s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m3s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m10s
CI/CD Pipeline / Unit Tests (push) Successful in 9m3s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 49s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-04 10:09:22 +08:00
xiaoxia 3687da034d feat(title): GeneratePage向PreviewVideoPanel传递titleText和titleSettings
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 E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 41s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m2s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m59s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m20s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m43s
AI Code Review / AI Code Review (pull_request) Successful in 2m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 3m5s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 41s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m13s
2026-08-04 10:03:35 +08:00
xiaoxia c29119e179 feat(title): PreviewVideoPanel新增标题实时预览区域,支持样式渲染 2026-08-04 10:03:27 +08:00
xiaoxia cc072bf22d fix(title): 预设样式不再绑定字号,applyPreset/getActivePreset去掉size匹配 2026-08-04 10:03:19 +08:00
xiaoxia a356639a34 fix: 修复模板查询方法名(repo.get替代repo.get_by_id) (#1231)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m3s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m25s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 1m48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m38s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m28s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m16s
CI/CD Pipeline / Integration Tests (push) Successful in 3m22s
CI/CD Pipeline / Unit Tests (push) Successful in 10m35s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m59s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 35s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 16s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m22s
2026-08-04 01:35:23 +08:00
xiaoxia 405bce6e1b fix: 预览分辨率自动从模板mode推断(Fix3补充) (#1230)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m5s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 1m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m50s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m3s
CI/CD Pipeline / Integration Tests (push) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 19s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m29s
CI/CD Pipeline / Unit Tests (push) Successful in 9m57s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-08-04 01:08:22 +08:00
xiaoxia 0e5f2eb036 Merge pull request 'fix: 预览视频3问题修复 — URL签名/时长约束/分辨率 (#1209)' (#1229) from fix/preview-generation-3-issues into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m34s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m30s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m11s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m46s
CI/CD Pipeline / Integration Tests (push) Successful in 1m27s
CI/CD Pipeline / Unit Tests (push) Successful in 9m4s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m44s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 15s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 35s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
2026-08-04 00:21:41 +08:00
123 changed files with 7602 additions and 4372 deletions
+7 -43
View File
@@ -21,9 +21,8 @@ on:
permissions:
contents: read
concurrency:
group: ci-pipeline-${{ gitea.event_name }}-${{ gitea.ref }}
# PR事件取消进行中的旧run,push事件不取消(确保完整CI跑完)
cancel-in-progress: ${{ gitea.event_name == 'pull_request' }}
group: ci-pipeline-${{ gitea.ref }}
cancel-in-progress: true
jobs:
check-frontend-only:
name: Check if frontend-only change
@@ -1087,26 +1086,7 @@ jobs:
- name: Run Playwright E2E on staging
shell: bash
run: |
set -eu
# DooD模式下不能用-v挂载(宿主机路径与CI容器路径不一致)
# 改用 docker create + docker cp 方式把代码拷进容器
CONTAINER_NAME="staging-e2e-$$"
# 强制清理可能残留的同名容器(上一次异常退出时未清理)
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
docker create --name "$CONTAINER_NAME" --ipc=host \
-e E2E_BASE_URL=https://staging.xiaoxiajianji.com \
-e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \
-e E2E_BROWSER_CHANNEL=chromium \
-e PLAYWRIGHT_HEADLESS=1 \
-w /workspace/apps/web \
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
sh -lc "npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts"
docker cp apps "$CONTAINER_NAME:/workspace/"
docker cp package-lock.json "$CONTAINER_NAME:/workspace/" 2>/dev/null || true
docker start -a "$CONTAINER_NAME"
EXIT_CODE=$(docker wait "$CONTAINER_NAME")
docker rm "$CONTAINER_NAME" 2>/dev/null || true
exit $EXIT_CODE
bash scripts/ci/run_staging_tests.sh e2e
- name: Job duration summary
if: always()
@@ -1153,22 +1133,7 @@ jobs:
- name: Run API integration tests on staging
shell: bash
run: |
set -eu
# DooD模式下不能用-v挂载(宿主机路径与CI容器路径不一致)
# 改用 docker create + docker cp 方式把代码拷进容器
CONTAINER_NAME="staging-api-tests-$$"
docker create --name "$CONTAINER_NAME" \
-e E2E_BASE_URL=https://staging.xiaoxiajianji.com \
-e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \
-w /workspace/apps/web \
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
sh -lc 'npm ci && npx playwright test --reporter=line e2e/test_auth.spec.ts e2e/test_asset.spec.ts e2e/test_project.spec.ts'
docker cp apps "$CONTAINER_NAME:/workspace/"
docker cp package-lock.json "$CONTAINER_NAME:/workspace/" 2>/dev/null || true
docker start -a "$CONTAINER_NAME"
EXIT_CODE=$(docker wait "$CONTAINER_NAME")
docker rm "$CONTAINER_NAME" 2>/dev/null || true
exit $EXIT_CODE
bash scripts/ci/run_staging_tests.sh api
- name: Job duration summary
if: always()
@@ -1495,9 +1460,8 @@ jobs:
shell: sh
run: bash scripts/ci/step_timer_start.sh
- name: Run production browser E2E
shell: sh
shell: bash
run: |
set -eu
docker run --rm --ipc=host \
-e E2E_BASE_URL=https://saas.xiaoxiajianji.com \
-e E2E_API_BASE=https://api.xiaoxiajianji.com/api/v1 \
@@ -1506,7 +1470,7 @@ jobs:
-v "$PWD:/workspace" \
-w /workspace/apps/web \
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
sh -lc 'npm ci && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts'
bash -c 'for i in 1 2 3; do npm ci --registry=https://registry.npmmirror.com && break; echo "npm ci attempt $i failed, retrying..."; sleep 15; done && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts e2e/core-titles.spec.ts'
- name: Job duration summary
if: always()
@@ -1862,4 +1826,4 @@ jobs:
[ "${{ steps.gate.outputs.gate_result }}" = "success" ] || STATUS="error"
START_TIME=""
[ -f /tmp/ci_job_start_time ] && START_TIME=$(cat /tmp/ci_job_start_time)
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
python3 scripts/ci/ci_trace_report.py --service xiaoxia-saas-ci --status $STATUS --start-time "$START_TIME" || true
+3 -6
View File
@@ -95,12 +95,9 @@ jobs:
set -eu
cd apps/web
# Config npm mirror for speed
npm config set registry https://registry.npmmirror.com
# Install dependencies with retry
for i in 1 2 3; do
npm ci --no-audit --no-fund && break
npm ci --registry=https://registry.npmmirror.com --no-audit --no-fund && break
echo "npm install failed, retry $i/3..."
[ $i -eq 3 ] && exit 1
rm -rf node_modules
@@ -109,12 +106,12 @@ jobs:
# TypeScript check
echo "=== TypeScript check ==="
npx --no-install tsc --noEmit
./node_modules/.bin/tsc --noEmit
# Vite build
echo "=== Vite build ==="
export VITE_API_URL=https://staging-api.xiaoxiajianji.com
npx --no-install vite build
./node_modules/.bin/vite build
echo "=== Build completed ==="
ls -la dist/
+222
View File
@@ -0,0 +1,222 @@
---
AIGC:
Label: "1"
ContentProducer: 001191110102MACQD9K64018705
ProduceID: 15868733686388_0/project_7655981463858544923-files/docs/1197_preview_generation_proposal.md
ReservedCode1: ""
ContentPropagator: 001191110102MACQD9K64028705
PropagateID: 15868733686388#1785468313901
ReservedCode2: ""
---
# #1197 预览生成接口方案评估
## 背景
智能剪辑「一键生成」流程中,第3步预览生成当前被跳过,直接进入下一步。需要实现真正的预览生成功能,让用户在正式生成前能看到效果预览。
## 现状分析
### 现有生成链路
```
API 触发生成 → GenerationTask入库 → Celery异步任务 → UnifiedRenderService渲染 → OSS上传 → 更新状态
```
**关键节点:**
1. **API层**`POST /generation-tasks``POST /templates/{id}/generate` 触发生成
2. **任务调度**Celery task `worker.generate_video`
3. **渲染引擎**`UnifiedRenderService`(统一渲染引擎,已接入9个效果层)
4. **输出配置**:默认 720p (1280x720),支持 `resolution` 字段自定义
5. **产物存储**`GeneratedVideo` 表记录,OSS 存储视频文件
### 已有可复用能力
| 能力 | 位置 | 是否可复用 |
|------|------|-----------|
| 任务创建与状态管理 | `GenerationTask` + `CreateGenerationTaskUseCase` | ✅ 是 |
| 素材下载与预处理 | `_download_video_assets` / `_download_voice_asset` | ✅ 是 |
| 统一渲染引擎 | `UnifiedRenderService` | ✅ 是 |
| 分辨率配置 | `resolution` 字段已支持 | ✅ 是 |
| 混音与后处理 | `_render_video` 内流程 | ✅ 是 |
| OSS 上传与查重 | `_upload_and_dedup` | ✅ 是 |
| 进度追踪 | `append_log` / `progress` 字段 | ✅ 是 |
## 方案对比
### 方案A:复用现有生成链路 + is_preview 标记(推荐)
**思路**:在现有 GenerationTask 上加 `is_preview` 标记,预览生成走完整链路但参数降级。
**改动点:**
1. **数据模型**`GenerationTask``is_preview: bool` 字段(默认 false);`GeneratedVideo``is_preview: bool`
2. **API 层**:生成接口加 `is_preview` 参数,预览任务不计入配额
3. **渲染参数**:预览模式下自动调整
- 分辨率:480p (854x480)
- 时长:限制前 15 秒(或模板第一个片段)
- 码率:降低至 1.5Mbps(正式 4Mbps
- 效果层:跳过高级转场/粒子特效等耗时效果
4. **任务调度**:预览任务走低优先级队列(或复用现有队列,标记优先级)
5. **前端对接**:预览生成结果带 `is_preview=true` 标记,前端展示"预览"标签
**优点:**
- 代码复用率 90%+,改动最小
- 与正式生成逻辑一致,预览效果真实可信
- 进度查询、结果展示等功能直接复用
- 后续可平滑升级:预览满意后一键转正式生成
**缺点:**
- 需要区分预览和正式任务,避免数据混淆
- 预览任务和正式任务竞争同一队列资源(可后续优化为独立队列)
**开发量估算**2-3 天
- 数据模型 + 迁移:0.5 天
- API 层改造:0.5 天
- 渲染参数降级:1 天
- 测试 + 联调:1 天
---
### 方案B:新建独立预览接口 + 轻量渲染逻辑
**思路**:新建独立的预览生成接口,使用简化的渲染逻辑(如只拼接素材+基础配音,跳过大部分效果)。
**改动点:**
1. 新增 `PreviewTask` 数据模型
2. 新增 `POST /api/v1/preview/generate` 接口
3. 新增独立的 Celery task `worker.generate_preview`
4. 简化渲染流程:只做素材裁剪+拼接+配音,跳过转场/滤镜/字幕特效等
**优点:**
- 完全隔离,不影响正式生成链路
- 可以做极致优化,预览生成速度快
- 数据模型清晰,不会混淆
**缺点:**
- 代码重复率高,两套生成逻辑维护成本翻倍
- 预览效果与正式生成可能不一致(效果层差异)
- 前端需要对接两套接口
- 无法从预览升级为正式生成(需重新走完整流程)
**开发量估算**4-5 天
- 数据模型 + 接口:1 天
- 简化渲染逻辑:2 天
- 测试 + 联调:1-2 天
---
### 方案C:图片预览(首帧/关键帧截图)
**思路**:不生成视频,只生成几张关键帧的预览图片。
**优点:**
- 生成速度极快(秒级)
- 资源消耗小
**缺点:**
- 预览效果差,用户无法感知动态效果
- 无法验证配音、转场、节奏等时间维度的效果
- 用户体验不佳,不如"真预览"有说服力
**开发量估算**1-2 天
---
## 推荐方案:方案A(复用现有生成链路)
### 核心理由
1. **效果保真**:预览和正式生成用同一套渲染引擎,效果一致,用户信任度高
2. **开发效率**90% 代码复用,2-3 天可上线
3. **可扩展性强**:后续可加「预览转正式」「低分辨率快速预览」等增强功能
4. **维护成本低**:一套生成逻辑,bug 修复和新功能同时生效
### 详细设计
#### 1. 数据模型变更
```python
# GenerationTask 新增字段
is_preview: bool = False
"""是否为预览生成"""
preview_of: str = ""
"""预览对应的正式任务 ID(或反向关联)"""
# GeneratedVideo 新增字段
is_preview: bool = False
"""是否为预览视频"""
```
**迁移**alembic 新增 migration,两个表各加 1-2 个字段。
#### 2. API 层
```
POST /api/v1/generation-tasks
Body 增加 is_preview: bool = false
POST /api/v1/templates/{id}/generate
Query 增加 is_preview: bool = false
```
**配额处理**:预览生成不计入用户配额,不占用生成次数限制。
#### 3. 渲染参数降级
| 参数 | 正式生成 | 预览生成 |
|------|---------|---------|
| 分辨率 | 720p (1280x720) | 480p (854x480) |
| 码率 | 4 Mbps | 1.5 Mbps |
| 时长 | 完整时长 | 前 15 秒(或第一段) |
| 帧率 | 30 fps | 24 fps |
| 转场效果 | 完整转场 | 仅淡入淡出(或简单切) |
| 特效滤镜 | 全部启用 | 跳过粒子/光效等高级效果 |
| 字幕 | 完整渲染 | 正常渲染(字幕是核心信息) |
| 配音 | 完整混音 | 正常混音(配音是核心信息) |
**实现方式**:在 `_render_video` 或 UnifiedRenderService 入口处,根据 `is_preview` 标记调整渲染配置。
#### 4. 任务调度
- 初期复用现有队列,预览任务正常排队
- 后续如需优化,可拆分独立预览队列(低优先级)
- 预览任务可设置较短超时时间
#### 5. 前端对接
- 调用生成接口时传 `is_preview=true`
- 结果列表中预览视频带「预览」标签
- 预览满意后可一键「升级为正式生成」(重新触发全分辨率生成,可复用素材下载缓存)
### 实施步骤
**Phase 1MVP2天):**
1. 数据模型 + 迁移
2. API 层支持 is_preview 参数
3. 渲染分辨率降级(480p
4. 不计入配额
5. 基础测试
**Phase 2(优化,1-2天):**
1. 时长限制(前15秒)
2. 效果层降级(跳高级效果)
3. 预览任务低优先级队列
4. 预览转正式生成功能
## 与前端对齐点
1. 预览生成的触发时机(第3步自动生成?用户点击才生成?)
2. 预览时长是固定15秒还是完整但低清?
3. 是否需要「预览转正式生成」功能
4. 预览视频的展示形态(和正式视频一样还是有特殊UI)
## 风险与注意事项
1. **数据混淆**:确保统计、计费、列表展示时正确区分预览和正式任务
2. **存储成本**:预览视频也占 OSS 空间,可设置自动清理(7天后自动删除)
3. **用户预期**:要明确告诉用户这是预览,效果和正式生成一致但清晰度低
4. **并发压力**:如果用户频繁生成预览,可能增加系统负载,需要限流
---
> 本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。
+382
View File
@@ -0,0 +1,382 @@
# #1197 预览生成接口技术方案(v2)
> 更新说明:v2 新增「多版本预览生成」能力,支持一个模板生成多个不重复的预览视频,左侧列表展示,用户可挑选满意的版本转正式生成。
## 1. 背景与目标
**现状**:智能剪辑「一键生成」第3步预览生成被跳过,用户直接进入正式生成,缺少效果预览环节。
**目标**
1. ✅ 实现真正的预览生成(低分辨率快速出片)
2.**支持生成 1~N 个不重复的预览版本**(默认 3 个),左侧列表展示
3. ✅ 预览满意后可一键转正式生成(复用素材下载缓存)
4. ✅ 不计入用户配额,不占用正式生成次数
---
## 2. 现有生成链路分析
### 2.1 链路总览
```
API 触发生成 → GenerationTask入库 → Celery异步任务
→ 下载素材 → 构建plan/clips → UnifiedRenderService渲染
→ 混音后处理 → OSS上传 + 查重 → 更新状态
```
### 2.2 决定视频差异的变量
要做"多个不重复版本",先分析哪些环节可以引入变化:
| 变量 | 当前行为 | 能否引入变化 | 影响程度 |
|------|---------|------------|---------|
| 素材选择 | 按 asset_ids 顺序全用 | ✅ 可随机选择子集/不同组合 | 大 |
| 素材排序 | 按 asset_ids 顺序 | ✅ 可 shuffle 重排 | 大 |
| 配音选择 | 固定 voice_library_id | ✅ 可选不同音色 | 中 |
| 标题选择 | 固定 title_ids 或随机选 | ✅ 可选不同标题 | 中 |
| BGM | 固定 bgm_config | ✅ 可选不同BGM | 小 |
| 转场效果 | 模板固定 | ✅ 可随机化转场类型 | 小 |
| 播放速度 | 模板固定 | ✅ 可微调速度 | 小 |
| 分辨率/码率 | 固定 | ✅ 预览可降级 | 不影响内容 |
### 2.3 可复用能力
- 任务创建与状态管理:`GenerationTask` + `CreateGenerationTaskUseCase`
- 素材下载与预处理:`_download_all_assets`
- 统一渲染引擎:`UnifiedRenderService`
- 分辨率配置:`resolution` 字段已支持
- 批量任务:`batch_id` 字段已存在(可用于预览组)
---
## 3. 总体方案:复用现有链路 + 多变体引擎
**核心思路**:沿用 v1 的"复用现有生成链路 + is_preview 标记"方案,在此基础上增加「多版本生成」能力。
**架构**
```
预览生成请求(count=N
创建预览批次(preview_batch
变体引擎生成 N 个变体参数(variation seed + 参数组合)
为每个变体创建 1 个 GenerationTaskis_preview=true
N 个 Celery 任务并行执行(走现有生成链路,参数降级)
N 个结果汇聚,前端左侧列表展示
```
---
## 4. 详细设计
### 4.1 数据模型变更
#### 4.1.1 GenerationTask 新增字段
```python
# 现有字段保留,新增:
is_preview: bool = False
"""是否为预览生成"""
preview_batch_id: str = ""
"""预览批次 ID(同批次的 N 个预览共享一个 batch)"""
variant_seed: int = 0
"""变体种子,用于控制随机化行为(素材选择、排序、转场等)"""
variant_params: dict = field(default_factory=dict)
"""变体参数快照(记录本次使用了哪些素材、标题、配音等,可追溯)
{
"asset_ids": [...], # 实际选用的素材子集
"title_id": "", # 选用的标题
"voice_id": "", # 选用的配音
"transition_style": "", # 转场风格
"bgm_track": "", # BGM 音轨
}
"""
```
#### 4.1.2 GeneratedVideo 新增字段
```python
is_preview: bool = False
"""是否为预览视频"""
preview_batch_id: str = ""
"""所属预览批次"""
variant_index: int = 0
"""在批次中的序号(0, 1, 2..."""
```
#### 4.1.3 迁移方案
alembic 新增 migration,两个表各加 4 个字段,默认值为空/false,无数据回填成本。
---
### 4.2 变体引擎(Variant Engine
**核心组件**:根据 count 和 seed,生成 N 组互不相同的生成参数。
#### 4.2.1 变纬度设计
| 维度 | 策略 | 说明 |
|------|------|------|
| **素材子集选择** | 从素材池中随机选 M 个(M=min(素材数, 模板clip数*2)) | 版本差异最大的来源 |
| **素材排序** | 随机打乱顺序 | 影响叙事节奏 |
| **标题选择** | 从 title_ids 中随机选 1 个 | 影响文案内容 |
| **配音选择** | 从 voice_ids 中随机选 1 个(如有多个) | 影响听觉体验 |
| **转场风格** | 从预设转场池中随机选 1 种 | 影响视觉过渡 |
| **BGM 选择** | 从 bgm 列表中随机选 1 首(如有配置) | 影响氛围 |
#### 4.2.2 去重机制
- 同一批次内,变体参数必须两两不同(至少素材组合或排序不同)
- 使用 `variant_seed` 保证可复现(相同 seed → 相同变体)
- 如果素材数量不足导致无法生成 N 个不同版本,按实际能生成的数量返回
#### 4.2.3 接口设计
```python
def generate_variants(
count: int,
seed: int,
asset_pool: list[str], # 可用素材 ID 列表
title_pool: list[str] = [], # 可用标题 ID 列表
voice_pool: list[str] = [], # 可用配音 ID 列表
template_id: str = "",
) -> list[dict]:
"""
生成 count 组变体参数。
每组参数包含:asset_ids(选用的素材+排序)、title_id、voice_id、
transition_style 等,确保两两不同。
"""
```
---
### 4.3 API 层设计
#### 4.3.1 预览生成接口
```
POST /api/v1/templates/{template_id}/generate-preview
```
**请求体**
```json
{
"asset_library_id": "lib_xxx",
"asset_ids": ["asset_1", "asset_2", ...],
"title_ids": ["title_1", "title_2"],
"voice_ids": ["voice_1", "voice_2"],
"bgm_config": {},
"count": 3,
"seed": 0
}
```
| 参数 | 类型 | 必填 | 默认 | 说明 |
|------|------|------|------|------|
| template_id | path | ✅ | - | 模板 ID |
| asset_library_id | body | ✅ | - | 素材库 ID |
| asset_ids | body | ✅ | - | 素材池(从中选子集/排序) |
| title_ids | body | - | [] | 标题池(可选,不传则不用标题) |
| voice_ids | body | - | [] | 配音池(可选) |
| bgm_config | body | - | {} | BGM 配置 |
| count | body | - | 3 | 生成几个预览版本(1~10) |
| seed | body | - | 0 | 随机种子,0 表示随机 |
**响应**
```json
{
"preview_batch_id": "pb_xxx",
"count": 3,
"tasks": [
{
"task_id": "gen_xxx_0",
"variant_index": 0,
"status": "processing"
},
{
"task_id": "gen_xxx_1",
"variant_index": 1,
"status": "processing"
},
...
]
}
```
#### 4.3.2 预览批次查询接口
```
GET /api/v1/preview-batches/{batch_id}
```
返回批次内所有预览任务的状态、结果(已完成的带 video_url)。
**响应**
```json
{
"preview_batch_id": "pb_xxx",
"count": 3,
"completed_count": 2,
"tasks": [
{
"task_id": "gen_xxx_0",
"variant_index": 0,
"status": "completed",
"video_url": "https://oss.xxx/preview/xxx.mp4",
"duration": 15.5,
"thumbnail_url": "https://oss.xxx/preview/xxx.jpg"
},
...
]
}
```
#### 4.3.3 预览转正式生成
```
POST /api/v1/preview-batches/{batch_id}/tasks/{task_id}/promote
```
将某个预览版本升级为正式生成(复用素材缓存,重新全分辨率渲染)。
---
### 4.4 渲染参数降级
预览模式下自动调整以下参数:
| 参数 | 正式生成 | 预览生成 |
|------|---------|---------|
| 分辨率 | 720p (1280x720) | 480p (854x480) |
| 码率 | 4 Mbps | 1.5 Mbps |
| 帧率 | 30 fps | 24 fps |
| 时长 | 完整时长 | 前 15 秒(或第一段完整clip) |
| 转场效果 | 完整转场 | 仅淡入淡出 |
| 高级特效 | 全部启用 | 跳过粒子/光效等 |
| 字幕 | 完整渲染 | 正常渲染 |
| 配音 | 完整混音 | 正常混音 |
| 输出质量 | high | medium |
**实现位置**`_render_video` 函数入口处,根据 `is_preview` 标记调整渲染配置。
---
### 4.5 任务调度
- **并行执行**:N 个预览任务并行提交到 Celery,不排队等待
- **低优先级**:预览任务走独立队列(`preview_queue`),不抢占正式生成资源
- **超时控制**:预览任务超时时间 5 分钟(正式 30 分钟)
- **自动清理**:预览视频 7 天后自动从 OSS 删除,任务记录标记为 archived
---
## 5. 前端对接要点
### 5.1 交互流程
```
第2步选素材 → 第3步点击"生成预览"
→ 显示 loading + 进度
→ 预览陆续完成,左侧列表逐张出现
→ 用户点击左侧不同版本,右侧预览区切换
→ 用户选中满意版本 → 点击"正式生成"
```
### 5.2 需要对齐的接口
1. **预览创建**`POST /templates/{id}/generate-preview`
2. **批次状态轮询**`GET /preview-batches/{id}`(建议 2s 轮询,或走 SSE
3. **预览转正式**`POST /preview-batches/{id}/tasks/{task_id}/promote`
### 5.3 数据格式对齐
预览视频条目结构:
```json
{
"id": "gen_xxx",
"variant_index": 0,
"status": "completed",
"video_url": "https://...",
"duration": 15.5,
"file_size": 2850000,
"thumbnail_url": "https://...",
"is_preview": true
}
```
---
## 6. 配额与计费
- 预览生成**不计入**用户配额
- 同一模板 + 同一素材池,每天最多生成 3 次多版本预览(防滥用)
- 单个预览批次最多 10 个版本
---
## 7. 实施步骤
### Phase 1:单版本预览(MVP2 天)
1. 数据模型 + 迁移(is_preview 字段)
2. API 层支持 is_preview 参数
3. 渲染分辨率降级(480p
4. 不计入配额
5. 基础测试
### Phase 2:多版本预览(3 天)
1. 变体引擎实现(素材随机选择 + 排序 + 去重)
2. preview_batch 批次管理
3. 批量创建 N 个预览任务
4. 批次查询接口
5. 前端联调
### Phase 3:预览转正式 + 优化(2 天)
1. 预览转正式生成接口(promote)
2. 素材下载缓存复用
3. 独立预览队列(低优先级)
4. 自动清理机制
5. 完整测试 + 压测
---
## 8. 风险与注意事项
| 风险 | 影响 | 应对 |
|------|------|------|
| 并发预览任务过多打满 worker | 正式生成被阻塞 | 独立预览队列 + 限流 |
| 变体生成的视频差异不够大 | 用户觉得"都一样" | 优先素材子集+排序差异,保证视觉差异 |
| 预览视频占用 OSS 存储 | 存储成本上升 | 7 天自动清理 + 低码率 |
| N 个版本同时下载重复素材 | 带宽浪费 | 批次内共享一次下载(Phase 3 优化) |
| 用户预期管理 | 以为预览就是最终效果 | 明确标注"预览版",说明分辨率差异 |
---
## 9. 开发量估算
| 阶段 | 后端 | 前端 | 合计 |
|------|------|------|------|
| Phase 1 单版本预览 | 2 天 | 1 天 | 3 天 |
| Phase 2 多版本预览 | 3 天 | 2 天 | 5 天 |
| Phase 3 转正式+优化 | 2 天 | 1 天 | 3 天 |
| **总计** | **7 天** | **4 天** | **~7 天(并行)** |
---
## 10. 与 v1 方案的差异总结
1. **新增多版本能力**:从"生成1个预览"升级为"生成N个不重复预览"
2. **新增变体引擎**:负责素材选择/排序/配音/标题的随机化
3. **新增批次概念**preview_batch 管理一组预览任务
4. **新增 promote 接口**:预览转正式生成
5. **独立队列**:预览不抢占正式生成资源
6. **开发量**:从 2-3 天增加到约 7 天(后端)
@@ -0,0 +1,82 @@
"""确认生成 API 改造:为 generation_tasks 表添加 source_task_id、output_width、output_height、cover_url、custom_title 字段
Revision ID: 054_confirm_gen_fields
Revises: 053_generation_task_is_preview
Create Date: 2026-08-16
Changes:
1. generation_tasks 表新增 source_task_id(来源预览任务 ID,带索引)
2. generation_tasks 表新增 output_width / output_height(动态输出分辨率)
3. generation_tasks 表新增 cover_url / custom_title(自定义封面和标题)
"""
import sqlalchemy as sa
from alembic import op
revision = "054_confirm_gen_fields"
down_revision = "053_generation_task_is_preview"
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
is_pg = conn.dialect.name == "postgresql"
if is_pg:
# 幂等检查:source_task_id 列是否已存在
result = conn.execute(
sa.text(
"SELECT column_name FROM information_schema.columns "
"WHERE table_name = 'generation_tasks' AND column_name = 'source_task_id'"
)
)
if result.scalar() is not None:
return
# source_task_id
op.add_column(
"generation_tasks",
sa.Column("source_task_id", sa.String(32), nullable=False, server_default=""),
)
# output_width
op.add_column(
"generation_tasks",
sa.Column("output_width", sa.Integer, nullable=False, server_default=sa.text("1280")),
)
# output_height
op.add_column(
"generation_tasks",
sa.Column("output_height", sa.Integer, nullable=False, server_default=sa.text("720")),
)
# cover_url
op.add_column(
"generation_tasks",
sa.Column("cover_url", sa.String(1000), nullable=False, server_default=""),
)
# custom_title
op.add_column(
"generation_tasks",
sa.Column("custom_title", sa.String(500), nullable=False, server_default=""),
)
# 索引
op.create_index(
"ix_generation_tasks_source_task_id",
"generation_tasks",
["source_task_id"],
)
def downgrade() -> None:
op.drop_index("ix_generation_tasks_source_task_id", table_name="generation_tasks")
op.drop_column("generation_tasks", "custom_title")
op.drop_column("generation_tasks", "cover_url")
op.drop_column("generation_tasks", "output_height")
op.drop_column("generation_tasks", "output_width")
op.drop_column("generation_tasks", "source_task_id")
+63 -6
View File
@@ -19,6 +19,9 @@ from app.schemas.asset import (
BatchTagRequest,
CreateAssetRequest,
ListAssetsResponse,
SmartMatchItem,
SmartMatchRequest,
SmartMatchResponse,
UpdateAssetRequest,
UpdateAssetReviewRequest,
)
@@ -30,6 +33,7 @@ from packages.application import (
CreateAssetUseCase,
)
from packages.domain import AssetStatus, ClassificationStatus
from packages.domain.smart_match import smart_select_assets
logger = logging.getLogger(__name__)
@@ -519,6 +523,51 @@ def batch_mark_assets(
)
@router.post("/smart-match", response_model=SmartMatchResponse)
def smart_match_assets(
request: SmartMatchRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
asset_repository: Any = Depends(get_asset_repository),
asset_library_repository: Any = Depends(get_asset_library_repository),
project_repository: Any = Depends(get_project_repository),
) -> SmartMatchResponse:
"""智能选素材:根据素材库内容,按质量分+时长均衡+新鲜度+未使用偏好综合评分,返回 Top N 素材。"""
library = asset_library_repository.get(request.library_id)
if library is None:
raise HTTPException(status_code=404, detail=f"AssetLibrary {request.library_id} not found")
check_project_access(library.project_id, authenticated_user.user.id, project_repository)
# 获取素材库中所有 ready 素材(DB 层按 kind 过滤,避免加载不必要的数据到内存)
# kind → file_type 映射:schema 已校验只允许 video/image/audio,与 file_type 一致
if request.kind:
filtered_assets = asset_repository.find_by_library_and_file_type(
request.library_id, request.kind, status=["ready"], limit=10000
)
else:
filtered_assets = asset_repository.find_by_library(
request.library_id, status=["ready"], limit=10000
)
total_candidates = len(filtered_assets)
# 调用统一智能选素材算法(kind 已在 DB 层过滤,无需重复过滤)
results = smart_select_assets(
filtered_assets,
limit=request.limit,
kind=None,
)
items = [
SmartMatchItem(
asset=_to_asset_response(r.asset),
score=r.score,
breakdown=r.breakdown,
)
for r in results
]
return SmartMatchResponse(items=items, total_candidates=total_candidates)
@router.get("/{asset_id}", response_model=AssetResponse)
def get_asset(
asset_id: str,
@@ -622,20 +671,28 @@ def create_asset(
asset_library_repository: Any = Depends(get_asset_library_repository),
project_repository: Any = Depends(get_project_repository),
) -> AssetResponse:
project = project_repository.find_by_id(request.project_id)
# 先获取素材库,用于推导 project_id(前端可能不传)
library = asset_library_repository.get(request.library_id)
if library is None:
raise HTTPException(status_code=404, detail=f"AssetLibrary {request.library_id} not found")
# project_id 自动推导:优先用请求值,否则从 library 关联的项目获取
project_id = request.project_id or library.project_id
project = project_repository.find_by_id(project_id)
if project is None:
raise HTTPException(status_code=404, detail=f"Project {request.project_id} not found")
raise HTTPException(status_code=404, detail=f"Project {project_id} not found")
if not project.can_access(authenticated_user.user.id):
raise HTTPException(status_code=403, detail="Access denied to project")
library = asset_library_repository.get(request.library_id)
if library is None or library.project_id != request.project_id:
raise HTTPException(status_code=404, detail=f"AssetLibrary {request.library_id} not found")
# 确保 library 和 project 归属一致
if library.project_id != project_id:
raise HTTPException(status_code=400, detail="AssetLibrary does not belong to the specified project")
use_case = CreateAssetUseCase(asset_repository)
item = use_case.execute(
CreateAssetCommand(
project_id=request.project_id,
project_id=project_id,
library_id=request.library_id,
name=request.name,
storage_key=request.storage_key,
+127 -7
View File
@@ -17,6 +17,7 @@ from app.core.task_enqueue import (
safe_enqueue_generation_task,
)
from app.dependencies import (
get_db_session,
get_generated_video_repository,
get_generation_task_repository,
)
@@ -25,7 +26,14 @@ from app.schemas.generation_task import (
PreviewGenerationTaskResponse,
)
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.edit_template_repository import (
SQLAlchemyEditTemplateRepository,
)
from packages.adapters.sqlalchemy_impl.template_repository import (
SQLAlchemyTemplateRepository,
)
from packages.application import (
CreateGenerationTaskCommand,
CreateGenerationTaskUseCase,
@@ -39,6 +47,13 @@ router = APIRouter()
PREVIEW_RESOLUTION = "854x480"
# 模板 mode → 视频比例映射
_TEMPLATE_MODE_TO_RATIO = {
"pip": "9:16",
"standard": "16:9",
"square": "1:1",
}
def _calc_preview_resolution(video_ratio: str = "") -> str:
"""根据视频比例计算预览分辨率(短边 480,长边按比例)。
@@ -55,6 +70,101 @@ def _calc_preview_resolution(video_ratio: str = "") -> str:
return ratio_map.get(video_ratio.strip(), PREVIEW_RESOLUTION)
def _infer_video_ratio_from_template(
template_id: str, db: Session, user_id: str = ""
) -> str:
"""从模板 mode 推断视频比例,前端未传 video_ratio 时使用。
Returns:
视频比例字符串(如 "9:16"),查询失败返回空字符串。
"""
if not template_id:
return ""
try:
repo = SQLAlchemyTemplateRepository(db)
template = repo.get(template_id, user_id)
if template:
mode = getattr(template, "mode", "") or ""
ratio = _TEMPLATE_MODE_TO_RATIO.get(mode.strip(), "")
if ratio:
logger.info(
"[预览生成] 从模板 mode=%s 推断 video_ratio=%s",
mode,
ratio,
)
return ratio
except Exception:
logger.warning(
"[预览生成] 查询模板失败,跳过 video_ratio 推断: template_id=%s",
template_id,
exc_info=True,
)
return ""
def _resolve_strategy_id_from_template(
template_id: str, db: Session, user_id: str = ""
) -> str:
"""从模板读取 editing_mode / mode 作为 strategy_id。
优先查新模板系统(EditTemplate.editing_mode),fallback 旧模板(Template.mode)。
Worker 端使用 strategy_id 作为渲染 mode,为空则默认 one_take。
"""
if not template_id:
return ""
# 优先查新模板系统
try:
new_repo = SQLAlchemyEditTemplateRepository(db)
new_template = new_repo.get(template_id)
if new_template and getattr(new_template, "editing_mode", ""):
mode = new_template.editing_mode.strip()
if mode:
logger.info(
"[预览生成] 从新模板 editing_mode=%s (template_id=%s)",
mode,
template_id,
)
# 画中画已下线,pip/voice_pip 统一映射为 one_take
if mode in ("pip", "voice_pip"):
logger.info("[预览生成] %s → one_take (画中画已下线)", mode)
mode = "one_take"
return mode
except Exception:
logger.debug(
"[预览生成] 新模板查询失败,尝试旧模板: template_id=%s",
template_id,
exc_info=True,
)
# fallback 旧模板系统
try:
old_repo = SQLAlchemyTemplateRepository(db)
old_template = old_repo.get(template_id, user_id)
if old_template:
mode = getattr(old_template, "mode", "") or ""
mode = mode.strip()
if mode:
logger.info(
"[预览生成] 从旧模板 mode=%s (template_id=%s)",
mode,
template_id,
)
# 画中画已下线,pip/voice_pip 统一映射为 one_take
if mode in ("pip", "voice_pip"):
logger.info("[预览生成] %s → one_take (画中画已下线)", mode)
mode = "one_take"
return mode
except Exception:
logger.warning(
"[预览生成] 旧模板查询也失败,strategy_id 留空: template_id=%s",
template_id,
exc_info=True,
)
return ""
def _mark_task_failed(repo, task, reason: str) -> None:
"""入队失败时将任务标记为 failed,避免产生僵尸 pending 数据。"""
try:
@@ -146,6 +256,7 @@ def create_preview_generation_task(
request: CreatePreviewGenerationTaskRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
generation_task_repository=Depends(get_generation_task_repository),
db: Session = Depends(get_db_session),
) -> PreviewGenerationTaskResponse:
"""创建预览生成任务。
@@ -159,10 +270,11 @@ def create_preview_generation_task(
"""
user_id = authenticated_user.user.id
logger.info(
"[预览生成] 接收请求: user_id=%s, template_id=%s, asset_count=%d",
"[预览生成] 接收请求: user_id=%s, template_id=%s, asset_count=%d, preview_count=%d",
user_id,
request.template_id,
len(request.asset_ids),
request.preview_count,
)
# 预检查队列限流
@@ -176,7 +288,7 @@ def create_preview_generation_task(
except UserPendingLimitExceeded as e:
raise HTTPException(
status_code=429,
detail=f"您的待处理任务过多(当前 {e.pending_count - 1}/{e.limit}),请等待完成后再提交",
detail=f"您的待处理任务过多(当前 {e.pending_count - 1}/{e.limit}),请等待后再提交",
) from e
except GlobalQueueFull as e:
raise HTTPException(
@@ -184,6 +296,14 @@ def create_preview_generation_task(
detail="系统繁忙,请稍后再试",
) from e
# 确定视频比例:优先前端传入,否则从模板 mode 推断
video_ratio = request.video_ratio or ""
if not video_ratio and request.template_id:
video_ratio = _infer_video_ratio_from_template(request.template_id, db, user_id)
# 从模板读取 editing_mode / mode 作为 strategy_id(渲染 pipeline 的 mode 参数)
strategy_id = _resolve_strategy_id_from_template(request.template_id, db, user_id)
use_case = CreateGenerationTaskUseCase(generation_task_repository)
try:
@@ -191,7 +311,7 @@ def create_preview_generation_task(
CreateGenerationTaskCommand(
project_id="",
asset_library_id="",
strategy_id="",
strategy_id=strategy_id,
voice_library_id="",
template_id=request.template_id,
asset_ids=list(request.asset_ids),
@@ -202,7 +322,7 @@ def create_preview_generation_task(
asset_select_mode="",
batch_id="",
video_title=request.video_title,
resolution=_calc_preview_resolution(request.video_ratio),
resolution=_calc_preview_resolution(video_ratio),
bgm_config=request.bgm_config or {},
auto_retry_enabled=False,
auto_retry_max=0,
@@ -214,7 +334,7 @@ def create_preview_generation_task(
raise HTTPException(status_code=400, detail=str(e)) from e
except Exception as e:
logger.error("[预览生成] 创建失败: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="创建预览生成任务失败,请稍后") from e
raise HTTPException(status_code=500, detail="创建预览生成任务失败,请稍后") from e
# 入队执行;若入队失败则标记任务为 failed 避免僵尸数据
try:
@@ -228,11 +348,11 @@ def create_preview_generation_task(
logger.warning("[预览生成] 任务入队失败: task_id=%s", task.id)
_mark_task_failed(generation_task_repository, task, "任务入队失败")
raise HTTPException(status_code=500, detail="任务入队失败,请稍后重试")
except UserPendingLimitExceeded:
except UserPendingLimitExceeded as e:
_mark_task_failed(generation_task_repository, task, "待处理任务超限")
raise HTTPException(
status_code=429,
detail="您的待处理任务过多,请等待完成后再提交",
detail=f"您的待处理任务过多(当前 {e.pending_count - 1}/{e.limit},请等待后再提交",
) from None
except GlobalQueueFull:
_mark_task_failed(generation_task_repository, task, "系统队列已满")
+109 -14
View File
@@ -26,6 +26,7 @@ from app.schemas.generated_video import (
)
from app.schemas.generation_task import (
BatchGenerationTaskResponse,
ConfirmGenerationRequest,
CreateGenerationTaskRequest,
GenerationTaskResponse,
ListGenerationTasksResponse,
@@ -38,6 +39,7 @@ from packages.application import (
GetGenerationTaskUseCase,
ListGeneratedVideosByTaskUseCase,
)
from packages.domain.smart_match import smart_select_assets
logger = logging.getLogger(__name__)
@@ -61,6 +63,12 @@ def _to_generation_task_response(task) -> GenerationTaskResponse:
video_title=getattr(task, "video_title", ""),
resolution=getattr(task, "resolution", ""),
bgm_config=getattr(task, "bgm_config", {}) or {},
is_preview=getattr(task, "is_preview", False),
source_task_id=getattr(task, "source_task_id", ""),
output_width=getattr(task, "output_width", 1280),
output_height=getattr(task, "output_height", 720),
cover_url=getattr(task, "cover_url", ""),
custom_title=getattr(task, "custom_title", ""),
logs=getattr(task, "logs", "[]"),
status=task.status,
progress=task.progress,
@@ -124,19 +132,11 @@ def _select_assets_from_library(
return [a.id for a in selected]
if mode == "smart":
# 智能匹配:按质量分降序 + 时长降序作为tiebreaker
# 注意:这里使用简单的 quality_score 排序保持向后兼容
# 更复杂的4维评分+多样性策略由 SmartAssetSelector 服务提供(用于 AI 精选等场景)
scored_assets = sorted(
ready_video_assets,
key=lambda a: (
-(a.quality_score if a.quality_score is not None else 0.0),
-(getattr(a, "duration", 0.0) or 0.0),
),
)
if count > 0:
scored_assets = scored_assets[:count]
return [a.id for a in scored_assets]
# 智能匹配:统一使用 packages/domain/smart_match.py 的多维评分+多样性选取
# 评分维度:质量分(40%) + 时长适配(30%) + 新鲜度(20%) + 未使用加分(10%)
limit = count if count > 0 else None
results = smart_select_assets(ready_video_assets, limit=limit, kind="video")
return [r.asset.id for r in results]
# 默认 all 模式:返回全部 ready 视频素材
return [a.id for a in ready_video_assets]
@@ -271,13 +271,19 @@ def create_generation_task(
detail="系统繁忙,请稍后再试",
) from e
# 画中画已下线:strategy_id 中的 pip/voice_pip 统一映射为 one_take
effective_strategy_id = request.strategy_id
if effective_strategy_id in ("pip", "voice_pip"):
logger.info("画中画已下线,strategy_id %s → one_take", effective_strategy_id)
effective_strategy_id = "one_take"
try:
for _ in range(count):
task = use_case.execute(
CreateGenerationTaskCommand(
project_id=project_id,
asset_library_id=asset_library_id,
strategy_id=request.strategy_id,
strategy_id=effective_strategy_id,
voice_library_id=request.voice_library_id,
template_id=request.template_id,
asset_ids=resolved_asset_ids,
@@ -292,6 +298,12 @@ def create_generation_task(
bgm_config=request.bgm_config,
auto_retry_enabled=request.auto_retry_enabled,
auto_retry_max=request.auto_retry_max,
is_preview=request.is_preview,
source_task_id=request.source_task_id,
output_width=request.output_width,
output_height=request.output_height,
cover_url=request.cover_url,
custom_title=request.custom_title,
)
)
try:
@@ -332,6 +344,83 @@ def create_generation_task(
return BatchGenerationTaskResponse(items=items, total=len(items))
@router.post("/tasks/{task_id}/confirm", response_model=BatchGenerationTaskResponse)
def confirm_generation(
task_id: str,
request: ConfirmGenerationRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
generation_task_repository: Any = Depends(get_generation_task_repository),
project_repository: Any = Depends(get_project_repository),
) -> BatchGenerationTaskResponse:
"""确认生成 — 基于预览任务创建正式生成任务。
查找预览任务,复制其配置,创建新的正式生成任务(is_preview=False),
使用高分辨率,复用 worker.generate_video 渲染路径。
"""
# 1. 查找源预览任务
source_task = generation_task_repository.get(task_id)
if source_task is None:
raise HTTPException(status_code=404, detail=f"Preview task {task_id} not found")
# 2. 权限检查
if source_task.created_by_user_id and source_task.created_by_user_id != authenticated_user.user.id:
raise HTTPException(status_code=403, detail="Access denied to this task")
if source_task.project_id:
check_project_access(source_task.project_id, authenticated_user.user.id, project_repository)
# 3. 创建正式生成任务,复制预览任务的配置
use_case = CreateGenerationTaskUseCase(generation_task_repository)
new_task = use_case.execute(
CreateGenerationTaskCommand(
project_id=source_task.project_id,
asset_library_id=source_task.asset_library_id,
strategy_id=source_task.strategy_id,
voice_library_id=source_task.voice_library_id,
template_id=source_task.template_id,
asset_ids=source_task.asset_ids,
title_ids=source_task.title_ids,
voice_ids=source_task.voice_ids,
created_by_user_id=authenticated_user.user.id,
source_edit_plan_id=source_task.source_edit_plan_id or "",
asset_select_mode=source_task.asset_select_mode,
video_title=getattr(source_task, "video_title", ""),
resolution=getattr(source_task, "resolution", ""),
is_preview=False,
source_task_id=task_id,
output_width=request.output_width,
output_height=request.output_height,
cover_url=request.cover_url,
custom_title=request.custom_title,
)
)
# 4. 调度 worker.generate_video(同一条渲染路径)
try:
if not safe_enqueue_generation_task(
new_task,
generation_task_repository,
user_id=authenticated_user.user.id,
log_prefix="[确认生成]",
log_task_status=True,
):
logger.warning("[确认生成] 入队失败: task_id=%s", new_task.id)
except UserPendingLimitExceeded:
raise HTTPException(
status_code=429,
detail="您的待处理任务过多,请等待完成后再提交",
) from None
except GlobalQueueFull:
raise HTTPException(
status_code=503,
detail="系统繁忙,请稍后再试",
) from None
return BatchGenerationTaskResponse(
items=[_to_generation_task_response(new_task)],
total=1,
)
@router.get("/tasks", response_model=ListGenerationTasksResponse)
def list_generation_tasks(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
@@ -429,6 +518,12 @@ def retry_generation_task(
asset_select_mode=getattr(task, "asset_select_mode", ""),
video_title=getattr(task, "video_title", ""),
resolution=getattr(task, "resolution", ""),
is_preview=getattr(task, "is_preview", False),
source_task_id=getattr(task, "source_task_id", ""),
output_width=getattr(task, "output_width", 1280),
output_height=getattr(task, "output_height", 720),
cover_url=getattr(task, "cover_url", ""),
custom_title=getattr(task, "custom_title", ""),
)
)
try:
@@ -98,6 +98,15 @@ def _auto_fallback_assign_assets(
clips_without_asset = [c for c in all_clips if not c.asset_id]
config_asset_ids = (plan_check.config or {}).get("asset_ids", [])
logger.info(
"模板编辑器自动兜底3 诊断: plan=%s total_clips=%d "
"clips_without_asset=%d config_asset_ids=%r",
plan_id,
len(all_clips),
len(clips_without_asset),
config_asset_ids[:5] if config_asset_ids else [],
)
if clips_without_asset and config_asset_ids:
logger.info(
"模板编辑器自动兜底3: plan=%s%d 个无素材片段分配 %d 个指定素材",
@@ -105,11 +114,42 @@ def _auto_fallback_assign_assets(
len(clips_without_asset),
len(config_asset_ids),
)
assigned = 0
for i, clip in enumerate(clips_without_asset):
asset_idx = i % len(config_asset_ids)
svc.assign_asset(clip.id, config_asset_ids[asset_idx])
logger.info("模板编辑器自动兜底3: plan=%s 素材分配完成", plan_id)
clips_without_asset = []
try:
svc.assign_asset(clip.id, config_asset_ids[asset_idx])
assigned += 1
except Exception as exc:
logger.error(
"模板编辑器自动兜底3: plan=%s clip=%s 分配素材 %s 失败: %s",
plan_id,
clip.id,
config_asset_ids[asset_idx],
exc,
)
logger.info(
"模板编辑器自动兜底3: plan=%s 素材分配完成 assigned=%d/%d",
plan_id,
assigned,
len(clips_without_asset),
)
# 重新检查剩余无素材片段
all_clips_after = svc.list_clips(plan_id)
clips_without_asset = [c for c in all_clips_after if not c.asset_id]
if clips_without_asset:
logger.warning(
"模板编辑器自动兜底3: plan=%s 仍有 %d 个片段无素材",
plan_id,
len(clips_without_asset),
)
elif not clips_without_asset:
logger.info("模板编辑器自动兜底3: plan=%s 所有片段已有素材,跳过", plan_id)
elif not config_asset_ids:
logger.info(
"模板编辑器自动兜底3: plan=%s config.asset_ids 为空,跳过分配",
plan_id,
)
return clips_without_asset
@@ -74,7 +74,7 @@ def generate_editor_draft(
plan_svc, plan_id, plan_check, clips_without_asset, asset_library_repo, asset_repo
)
# 检查是否可生成
# 检查是否可生成(含最后防线自动修复 + 诊断日志)
try:
can_gen, reason = plan_svc.can_generate(plan_id)
except ValueError as exc:
+28 -1
View File
@@ -2,7 +2,7 @@ from pydantic import BaseModel, Field
class CreateAssetRequest(BaseModel):
project_id: str = Field(..., min_length=1)
project_id: str | None = Field(default=None, description="可选,不传时从 library.project_id 自动推导")
library_id: str = Field(..., min_length=1)
name: str = Field(..., min_length=1, max_length=100)
storage_key: str = Field(..., min_length=1, max_length=255)
@@ -101,3 +101,30 @@ class ListAssetsResponse(BaseModel):
total: int = Field(default=0, ge=0)
skip: int = Field(default=0, ge=0)
limit: int = Field(default=100, ge=1)
class SmartMatchRequest(BaseModel):
"""智能选素材请求。"""
library_id: str = Field(..., min_length=1, description="素材库 ID")
limit: int | None = Field(default=None, ge=1, le=200, description="最大返回数量,不传则返回全部匹配素材")
kind: str | None = Field(
default=None,
pattern="^(video|image|audio)$",
description="按文件类型过滤,不传则返回所有类型",
)
class SmartMatchItem(BaseModel):
"""智能选素材结果条目。"""
asset: AssetResponse
score: float = Field(..., ge=0, le=100, description="综合得分 0-100")
breakdown: dict[str, float] = Field(default_factory=dict, description="各维度得分明细")
class SmartMatchResponse(BaseModel):
"""智能选素材响应。"""
items: list[SmartMatchItem]
total_candidates: int = Field(default=0, ge=0, description="参与评分的候选素材总数")
+28
View File
@@ -4,6 +4,15 @@ from datetime import datetime
from pydantic import BaseModel, Field, field_validator, model_validator
class ConfirmGenerationRequest(BaseModel):
"""确认生成请求体 — 基于预览任务创建正式生成任务"""
output_width: int = Field(default=1080, description="输出视频宽度")
output_height: int = Field(default=1920, description="输出视频高度")
cover_url: str = Field(default="", description="自定义封面图片 URL")
custom_title: str = Field(default="", description="自定义视频标题")
class CreateGenerationTaskRequest(BaseModel):
"""创建生成任务请求。
@@ -57,6 +66,13 @@ class CreateGenerationTaskRequest(BaseModel):
default_factory=dict,
description="自定义BGM配置,覆盖模板BGM设置。支持 enabled/source/asset_id/preset_id/audio_url/volume 等字段",
)
# ── 预览 / 确认生成 ──
is_preview: bool = Field(default=False, description="是否为预览任务")
source_task_id: str = Field(default="", description="来源预览任务 ID(确认生成时传入)")
output_width: int = Field(default=1280, description="输出视频宽度")
output_height: int = Field(default=720, description="输出视频高度")
cover_url: str = Field(default="", description="封面图片 URL")
custom_title: str = Field(default="", description="自定义视频标题")
@model_validator(mode="after")
def _check_at_least_one_mode(self) -> "CreateGenerationTaskRequest":
@@ -87,6 +103,12 @@ class GenerationTaskResponse(BaseModel):
video_title: str = ""
resolution: str = ""
bgm_config: dict = Field(default_factory=dict)
is_preview: bool = False
source_task_id: str = ""
output_width: int = 1280
output_height: int = 720
cover_url: str = ""
custom_title: str = ""
status: str
progress: float
result_count: int
@@ -146,6 +168,12 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
default_factory=dict,
description="自定义BGM配置,覆盖模板BGM设置。支持 enabled/source/asset_id/preset_id/audio_url/volume 等字段",
)
preview_count: int = Field(
default=1,
ge=1,
le=10,
description="预览视频生成数量,范围 1-10,默认 1",
)
@model_validator(mode="after")
def _check_template_id(self) -> "CreatePreviewGenerationTaskRequest":
+74 -5
View File
@@ -571,6 +571,10 @@ class EditPlanService:
def can_generate(self, plan_id: str) -> tuple[bool, str]:
"""检查是否可以触发渲染
包含最后一道防线的自动修复:
- 如果 clips 存在但都没有 asset_id,且 config.asset_ids 非空,
直接在内部执行素材分配,不再依赖前置 fallback 链路。
Returns:
tuple: (can_generate, reason)
"""
@@ -585,10 +589,69 @@ class EditPlanService:
if not clips:
return False, "请先添加片段后再生成视频"
# 检查是否至少有一个片段分配了素材
has_asset = any(c.asset_id for c in clips)
config_asset_ids_count = len((plan.config or {}).get("asset_ids", []))
clips_with_asset_count = sum(1 for c in clips if c.asset_id)
logger.info(
"can_generate 诊断: plan=%s status=%s total_clips=%d " "clips_with_asset=%d config_asset_ids_count=%d",
plan_id,
plan.status,
len(clips),
clips_with_asset_count,
config_asset_ids_count,
)
if not has_asset:
# ── 最后防线:自动从 config.asset_ids 分配素材 ──
config_asset_ids = (plan.config or {}).get("asset_ids", [])
if config_asset_ids:
logger.warning(
"can_generate 最后防线触发: plan=%s clips=%d 均无素材," "从 config.asset_ids(%d个) 自动分配",
plan_id,
len(clips),
len(config_asset_ids),
)
clips_without_asset = [c for c in clips if not c.asset_id]
assigned_count = 0
for i, clip in enumerate(clips_without_asset):
asset_idx = i % len(config_asset_ids)
try:
self.assign_asset(clip.id, config_asset_ids[asset_idx])
assigned_count += 1
except Exception as exc:
logger.warning(
"can_generate 最后防线: plan=%s clip=%s 分配素材 %s 失败: %s",
plan_id,
clip.id,
config_asset_ids[asset_idx],
exc,
)
logger.info(
"can_generate 最后防线: plan=%s 已为 %d/%d 个片段分配素材",
plan_id,
assigned_count,
len(clips_without_asset),
)
# 重新加载 clips 验证分配结果
clips = self._clip_repo.list_by_plan(plan_id)
if not any(c.asset_id for c in clips):
return False, "没有可渲染的就绪片段,自动修复后仍未分配素材"
else:
logger.warning(
"can_generate 失败: plan=%s clips=%d 均无素材," "且 config.asset_ids 为空,无法自动修复",
plan_id,
len(clips),
)
return False, "没有可渲染的就绪片段,请确保已选择素材"
return True, ""
def mark_clips_ready(self, plan_id: str) -> int:
"""所有 pending 状态的片段标记为 ready
"""已分配素材的 pending 片段标记为 ready
只标记同时满足以下条件的片段:
- status == PENDING
- asset_id 非空(已分配素材)
Returns:
int: 标记的片段数量
@@ -599,10 +662,16 @@ class EditPlanService:
)
count = 0
for clip in clips:
clip.mark_ready()
self._clip_repo.update(clip)
count += 1
logger.info("标记片段就绪: plan_id=%s count=%d", plan_id, count)
if clip.asset_id:
clip.mark_ready()
self._clip_repo.update(clip)
count += 1
logger.info(
"标记片段就绪: plan_id=%s marked=%d total_pending=%d",
plan_id,
count,
len(clips),
)
return count
def update_plan_config(self, plan_id: str, config_updates: Dict[str, Any]) -> EditPlan:
@@ -49,9 +49,10 @@ class PlanGeneratorService:
基于模板 + 素材,自动生成 EditPlan 及 EditPlanClip 列表。
"""
def __init__(self, db: Session) -> None:
def __init__(self, db: Session, asset_repo=None) -> None:
self._plan_repo = SQLAlchemyEditPlanRepository(db)
self._clip_repo = SQLAlchemyEditPlanClipRepository(db)
self._asset_repo = asset_repo
# ── 公开接口 ─────────────────────────────────────────────────────────────
@@ -64,6 +65,7 @@ class PlanGeneratorService:
project_id: str = "",
created_by_user_id: str = "",
name: str = "",
random_preview: bool = False,
) -> dict[str, Any]:
"""基于模板+素材生成剪辑计划
@@ -74,6 +76,7 @@ class PlanGeneratorService:
project_id: 所属项目 ID
created_by_user_id: 创建者用户 ID
name: 计划名称(为空则自动取模板名)
random_preview: 是否启用随机预览模式(随机选素材+随机截取片段)
Returns:
dict: {"plan": EditPlan, "clips": List[EditPlanClip]}
@@ -115,7 +118,17 @@ class PlanGeneratorService:
# 4. 按 editing_mode 分配素材
if asset_ids:
self._distribute_assets(clips, asset_ids, editing_mode)
# 如果是随机预览模式,获取素材时长信息
asset_durations = None
if random_preview and self._asset_repo:
asset_durations = self._fetch_asset_durations(asset_ids)
self._distribute_assets(
clips,
asset_ids,
editing_mode,
random_selection=random_preview,
asset_durations=asset_durations,
)
# 5. 持久化所有 clips 并计算总时长
created_clips: List[EditPlanClip] = []
@@ -199,9 +212,34 @@ class PlanGeneratorService:
clips: List[EditPlanClip],
asset_ids: List[str],
editing_mode: str,
*,
random_selection: bool = False,
asset_durations: dict[str, float] | None = None,
) -> None:
"""按 editing_mode 将素材分配到 clips(就地修改,未持久化).
委托给 plan_generator_utils.distribute_assets 纯函数。
"""
distribute_assets(clips, asset_ids, editing_mode)
distribute_assets(
clips,
asset_ids,
editing_mode,
random_selection=random_selection,
asset_durations=asset_durations,
)
def _fetch_asset_durations(self, asset_ids: List[str]) -> dict[str, float]:
"""从数据库获取素材时长信息.
Args:
asset_ids: 素材 ID 列表
Returns:
dict: 素材 ID -> 时长(秒)映射
"""
durations: dict[str, float] = {}
for asset_id in asset_ids:
asset = self._asset_repo.get(asset_id)
if asset and hasattr(asset, "duration"):
durations[asset_id] = float(asset.duration or 0.0)
return durations
@@ -1,164 +0,0 @@
"""SmartAssetSelector — 智能素材选择服务.
根据多维度评分从素材库中自动选择最优视频素材,
用于一键生成等需要自动选取素材的场景。
评分维度(加权求和,总分 0-1):
- 质量分(quality_score):权重 0.5 — 来自人工或AI的质量评分
- 分辨率适配:权重 0.2 — 分辨率越接近 1080p 得分越高
- 时长合理性:权重 0.2 — 3-30 秒区间最佳,过短/过长扣分
- 码率质量:权重 0.1 — 用文件大小/时长估算,码率适中得分高
特性:
- 最低质量分门槛:自动过滤低质量素材
- 时长多样性:保证选出的素材时长分布均匀(短/中/长各占一定比例)
- 兼容全部模式:素材库模式和项目模式都可用
纯逻辑部分已抽离到 packages.domain.asset_scoring。
"""
from __future__ import annotations
import logging
from packages.domain.asset_scoring import MEDIUM_BUCKET_MAX as _MEDIUM_BUCKET_MAX # noqa: F401 - re-export for tests
from packages.domain.asset_scoring import SHORT_BUCKET_MAX as _SHORT_BUCKET_MAX # noqa: F401 - re-export for tests
from packages.domain.asset_scoring import (
AssetScoreDetail,
SmartSelectResult,
diverse_selection,
filter_candidates,
score_asset_detail,
)
logger = logging.getLogger(__name__)
class SmartAssetSelector:
"""智能素材选择器.
从一组素材中按综合评分选择最优的 N 个,
同时保证时长分布的多样性。
"""
def __init__(
self,
min_quality_score: float = 30.0,
target_width: int = 1920,
target_height: int = 1080,
):
self.min_quality_score = min_quality_score
self.target_width = target_width
self.target_height = target_height
# ── 公开方法 ──────────────────────────────────────────────────────────────
def select(
self,
assets: list,
count: int = 0,
*,
ensure_diversity: bool = True,
) -> SmartSelectResult:
"""从素材列表中智能选择最优素材.
Args:
assets: Asset 实体列表(需要有 id/quality_score/width/height/duration/file_size 属性)
count: 选取数量,0 表示全部符合条件的
ensure_diversity: 是否保证时长多样性(默认开启)
Returns:
SmartSelectResult 选择结果
"""
# 1. 过滤:只保留 ready 状态的视频素材 + 最低质量分门槛
candidates, filtered_out = filter_candidates(assets, self.min_quality_score)
if not candidates:
return SmartSelectResult(
selected_ids=[],
total_candidates=0,
filtered_out=filtered_out,
avg_score=0.0,
details=[],
)
# 2. 对每个候选素材评分
scored: list[AssetScoreDetail] = []
for asset in candidates:
detail = score_asset_detail(
asset_id=asset.id,
quality=getattr(asset, "quality_score", None),
width=getattr(asset, "width", None),
height=getattr(asset, "height", None),
duration=getattr(asset, "duration", None),
file_size=getattr(asset, "file_size", 0) or 0,
target_width=self.target_width,
target_height=self.target_height,
)
scored.append(detail)
# 3. 按总分降序排列
scored.sort(key=lambda d: d.total_score, reverse=True)
# 4. 多样性选择(如果需要且数量有限制)
if ensure_diversity and count > 0 and len(scored) > count:
selected = diverse_selection(scored, count)
else:
# 无数量限制或不要求多样性,直接按排名取
selected = scored if count <= 0 else scored[:count]
avg_score = sum(d.total_score for d in selected) / len(selected) if selected else 0.0
result = SmartSelectResult(
selected_ids=[d.asset_id for d in selected],
total_candidates=len(candidates),
filtered_out=filtered_out,
avg_score=avg_score,
details=selected,
)
logger.info(
"智能素材选择完成: 候选=%d, 过滤=%d, 选中=%d, 平均分=%.3f",
result.total_candidates,
result.filtered_out,
len(result.selected_ids),
result.avg_score,
)
return result
# ── 向后兼容:私有方法别名(委托给 asset_scoring 纯函数) ────────────────
def _score_asset(self, asset) -> AssetScoreDetail:
"""对单个素材进行多维度评分(向后兼容)."""
return score_asset_detail(
asset_id=asset.id,
quality=getattr(asset, "quality_score", None),
width=getattr(asset, "width", None),
height=getattr(asset, "height", None),
duration=getattr(asset, "duration", None),
file_size=getattr(asset, "file_size", 0) or 0,
target_width=self.target_width,
target_height=self.target_height,
)
def _score_resolution(self, width: int | None, height: int | None) -> float:
"""分辨率评分(向后兼容)."""
from packages.domain.asset_scoring import score_resolution
return score_resolution(width, height, self.target_width, self.target_height)
def _score_duration(self, duration: float | None) -> float:
"""时长评分(向后兼容)."""
from packages.domain.asset_scoring import score_duration
return score_duration(duration)
def _score_bitrate(self, file_size: int, duration: float | None) -> float:
"""码率评分(向后兼容)."""
from packages.domain.asset_scoring import score_bitrate
return score_bitrate(file_size, duration)
def _diverse_selection(self, scored: list[AssetScoreDetail], count: int) -> list[AssetScoreDetail]:
"""多样性选择(向后兼容)."""
return diverse_selection(scored, count)
+21 -12
View File
@@ -196,24 +196,33 @@ test.describe("Core generation flow", () => {
await materialLabel.locator("input[type='checkbox']").check()
await page.getByRole("button", { name: "下一步" }).click()
// Step 3: preview (纯展示页,AI 智能匹配预览)
await expect(page.getByRole("heading", { name: /生成预览/ })).toBeVisible()
// Step 3: voice (可选步骤,新注册用户无配音素材,直接跳过)
await expect(page.getByRole("heading", { name: /选择配音/ })).toBeVisible({ timeout: 15000 })
await page.getByRole("button", { name: "下一步" }).click()
// Step 4: title
await expect(page.getByRole("heading", { name: /选择标题/ })).toBeVisible()
// Step 4: preview — 需要先生成预览视频,才能进入下一步
await expect(page.getByRole("heading", { name: /生成预览/ })).toBeVisible({ timeout: 15000 })
// 点击"生成预览"按钮触发预览生成
await page.locator(".xx-preview-generate-btn").click()
// 等待预览生成完成(后端渲染,可能需要较长时间)
await expect(page.getByText("预览生成成功")).toBeVisible({ timeout: 120_000 })
await page.getByRole("button", { name: "下一步" }).click()
// Step 5: title
await expect(page.getByRole("heading", { name: /选择标题/ })).toBeVisible({ timeout: 15000 })
// 如果 AI 自动选择标题模式开启,先切换到手动模式以显示输入框
const aiSwitch = page.locator(".xx-title-ai-toggle .xx-switch.active")
if (await aiSwitch.isVisible({ timeout: 2000 }).catch(() => false)) {
await aiSwitch.click()
// 等待输入框出现(条件渲染,需要等待 DOM 更新)
await expect(page.getByPlaceholder("输入或从标题库选择…")).toBeVisible({ timeout: 5000 })
}
const titleText = `E2E Test ${suffix}`
await page.getByPlaceholder("输入自定义标题…").fill(titleText)
await page.getByRole("button", { name: "下一步" }).click()
// Step 5: voice
await expect(page.getByRole("heading", { name: /选择配音/ })).toBeVisible()
const firstVoiceCard = page.locator(".xx-voice-choice-item").first()
await firstVoiceCard.click()
await page.getByPlaceholder("输入或从标题库选择…").fill(titleText)
await page.getByRole("button", { name: "下一步" }).click()
// Step 6: cover (默认 AI 智能选帧模式,直接下一步)
await expect(page.getByRole("heading", { name: /选择封面/ })).toBeVisible()
await expect(page.getByRole("heading", { name: /选择封面/ })).toBeVisible({ timeout: 15000 })
await page.getByRole("button", { name: "下一步" }).click()
// Step 7: confirm and generate
+14 -26
View File
@@ -1847,7 +1847,7 @@
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
@@ -1937,7 +1937,7 @@
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
@@ -3112,7 +3112,7 @@
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
@@ -4028,18 +4028,6 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"optional": true,
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -4468,7 +4456,7 @@
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
@@ -5010,7 +4998,7 @@
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
@@ -5026,7 +5014,7 @@
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
@@ -5038,14 +5026,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/pretty-format/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@@ -5743,6 +5723,14 @@
"react": "^18.3.1"
}
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+11
View File
@@ -49,6 +49,17 @@ export const getAssetsByKind = async (
return response.data.items || []
}
/**
* 智能匹配素材(后端 AI 选素材)
* 调用后端 smart-match 端点,由后端根据素材库内容智能选择素材
*/
export const smartMatchAssets = async (libraryId: string): Promise<{ items: AssetItem[] }> => {
const response = await apiClient.post("/assets/smart-match", {
library_id: libraryId,
})
return response.data
}
/** 创建素材(上传文件后调用,附带 metadata) */
export const createAsset = async (data: {
library_id: string
+2 -1
View File
@@ -29,10 +29,11 @@ export {
deleteAssetLibrary,
} from "./libraries"
// 素材 CRUD
// 素材 CRUD + 智能匹配
export {
getAssets,
getAssetsByKind,
smartMatchAssets,
createAsset,
updateAsset,
updateAssetReviewStatus,
@@ -18,6 +18,7 @@ const mapAssetToMediaAsset = (asset: AssetItem): MediaAsset => {
id: asset.id,
name: asset.name,
type: inferMediaType(asset.mime_type || ""),
source_url: asset.file_url,
thumbnail_url: asset.thumbnail_url,
duration: asset.duration ?? metaDuration,
size: asset.file_size ?? undefined,
@@ -424,6 +424,8 @@ export interface MediaAsset {
id: string
name: string
type: "video" | "image" | "audio"
/** 源文件 URL(视频/原图),用于悬浮预览播放 */
source_url?: string
/** 缩略图 URL */
thumbnail_url?: string
/** 时长(秒),仅 video/audio */
@@ -456,7 +456,8 @@
margin-bottom: 8px;
}
.as-preview-overlay-thumb img {
.as-preview-overlay-thumb img,
.as-preview-overlay-thumb video {
width: 100%;
height: 100%;
object-fit: cover;
@@ -12,7 +12,16 @@ const PreviewOverlay: React.FC<PreviewOverlayProps> = ({ asset, position }) => {
return (
<div className="as-preview-overlay" style={{ left: position.x, top: position.y }}>
<div className="as-preview-overlay-thumb">
{asset.thumbnail_url ? (
{asset.type === "video" && asset.source_url ? (
<video
src={asset.source_url}
autoPlay
muted
loop
playsInline
poster={asset.thumbnail_url}
/>
) : asset.thumbnail_url ? (
<img src={asset.thumbnail_url} alt={asset.name} />
) : (
<span className="as-preview-overlay-thumb-icon">{MATERIAL_TYPE_ICONS[asset.type]}</span>
@@ -60,7 +60,6 @@ export const useTtsPanel = ({ open, config, onChange, onClose }: UseTtsPanelOpti
return () => {
active = false
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
/* ── 组件卸载时清理音频资源 ── */
+65 -37
View File
@@ -1,6 +1,6 @@
/**
* 智能剪辑页面 — V21 原型 1:1 还原
* 7 步向导:选择模板 → 选择素材 → 生成预览 → 选择标题 → 选择配音 → 选择封面 → 确认生成
* 智能剪辑页面 — V22 多预览 + 配音前置
* 7 步向导:选择模板 → 选择素材 → 选择配音 → 生成预览 → 选择标题 → 选择封面 → 确认生成
* 左右布局:左侧 generate-form + 右侧 generate-preview
* 主组件仅保留整体布局与事件编排
* 状态管理 → hooks/useGenerateFormState
@@ -9,7 +9,7 @@
* 底部按钮 → components/GenerateStepActions
* 生成核心逻辑 → hooks/useGenerateVideo
*/
import React from "react"
import React, { useState, useMemo } from "react"
import { Modal, message } from "antd"
import { useNavigate } from "react-router-dom"
import type { VoiceClone } from "@/api/voice-clone"
@@ -24,7 +24,7 @@ import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState"
import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { useStep3Preview } from "./hooks/useStep3Preview"
import { useStep4Preview } from "./hooks/useStep4Preview"
import "./generate.css"
const GeneratePage: React.FC = () => {
@@ -80,8 +80,24 @@ const GeneratePage: React.FC = () => {
message.success("音色克隆成功!")
}
/* ── Step3 预览生成 ── */
const step3Preview = useStep3Preview({
/* ── 预览数量(多预览) ── */
const [previewCount, setPreviewCount] = useState(1)
/* ── 根据 voiceMode 构建 voiceIds 传给预览接口 ── */
/* selectedVoice / selectedClonedVoice 均为 string 类型(voice ID),
见 useGenerateFormState 返回值类型定义 */
const previewVoiceIds = useMemo((): string[] => {
if (voiceMode === "clone") {
const id: string = selectedClonedVoice
return id ? [id] : []
}
// preset / custom 模式
const id: string = selectedVoice
return id ? [id] : []
}, [voiceMode, selectedVoice, selectedClonedVoice])
/* ── Step4 预览生成(多预览 + voice_ids ── */
const step4Preview = useStep4Preview({
templates: userTemplates,
selectedTemplate,
materialMode,
@@ -89,6 +105,8 @@ const GeneratePage: React.FC = () => {
smartSelectedIds,
duration,
videoRatio,
voiceIds: previewVoiceIds,
previewCount,
})
/* ── 步骤导航 ── */
@@ -100,7 +118,7 @@ const GeneratePage: React.FC = () => {
selectedMaterials,
smartSelectedIds,
titleSettings,
previewReady: step3Preview.canProceed,
previewReady: step4Preview.canProceed,
})
/* ── 视频生成核心逻辑 ── */
@@ -187,14 +205,20 @@ const GeneratePage: React.FC = () => {
onDismissError={handleDismissError}
presetVoices={presetVoices}
videoRatio={videoRatio}
previewStatus={step3Preview.previewStatus}
previewResult={step3Preview.previewResult}
previewError={step3Preview.previewError}
previewTemplateName={step3Preview.templateName}
previewMaterialCount={step3Preview.materialCount}
previewProgress={step3Preview.progress}
onGeneratePreview={step3Preview.generatePreview}
onRegeneratePreview={step3Preview.regeneratePreview}
/* Step4 多预览 */
previewCount={previewCount}
onPreviewCountChange={setPreviewCount}
previewItems={step4Preview.items}
previewSelectedIndex={step4Preview.selectedIndex}
onSelectPreview={step4Preview.setSelectedIndex}
previewOverallStatus={step4Preview.previewStatus}
previewOverallError={step4Preview.previewError}
previewOverallProgress={step4Preview.progress}
previewAnyGenerating={step4Preview.anyGenerating}
previewTemplateName={step4Preview.templateName}
previewMaterialCount={step4Preview.materialCount}
onGeneratePreview={step4Preview.generatePreview}
onRegeneratePreview={step4Preview.regeneratePreview}
/>
<GenerateStepActions
@@ -210,33 +234,37 @@ const GeneratePage: React.FC = () => {
{/* ════ 右侧:预览 + 生成结果 ════ */}
<div className="xx-generate-right-col">
{/* 预览视频面板(Step3+ 常驻) */}
{currentStep >= 3 && (
{/* 预览视频面板(Step4+ 常驻,展示选中的预览 */}
{currentStep >= 4 && (
<PreviewVideoPanel
previewStatus={step3Preview.previewStatus}
previewResult={step3Preview.previewResult}
previewError={step3Preview.previewError}
progress={step3Preview.progress}
previewStatus={step4Preview.previewStatus}
previewResult={step4Preview.previewResult}
previewError={step4Preview.previewError}
progress={step4Preview.progress}
videoRatio={videoRatio}
onRegenerate={step3Preview.regeneratePreview}
onRegenerate={step4Preview.regeneratePreview}
titleText={titleSettings.title}
titleSettings={currentStep >= 5 ? titleSettings : undefined}
/>
)}
{/* 正式生成结果 */}
<GenerateResultPanel
generated={generated}
generating={generating}
progress={progress}
generateError={generateError}
generatedVideos={generatedVideos}
onVideoPreview={(video) => {
setPreviewVideo(video)
setPreviewModalOpen(true)
}}
onDownload={handleDownload}
onShare={handleShare}
onGoToLibrary={() => navigate("/app/products")}
/>
{/* 正式生成结果Step5+ 才显示) */}
{currentStep >= 5 && (
<GenerateResultPanel
generated={generated}
generating={generating}
progress={progress}
generateError={generateError}
generatedVideos={generatedVideos}
onVideoPreview={(video) => {
setPreviewVideo(video)
setPreviewModalOpen(true)
}}
onDownload={handleDownload}
onShare={handleShare}
onGoToLibrary={() => navigate("/app/products")}
/>
)}
</div>
</div>
@@ -1,6 +1,7 @@
/**
* GeneratePage 步骤内容渲染
* 根据当前步骤渲染对应的 Step 组件
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 预览(4) → 标题(5) → 封面(6) → 确认(7)
*/
import React from "react"
import type { EditingTemplate } from "@/api/editing-planner"
@@ -8,12 +9,12 @@ import type { PresetVoiceItem } from "@/api/voices"
import type { VoiceClone } from "@/api/voice-clone"
import type { CoverConfig } from "../../editing-planner/types"
import type { TitleSettings } from "../types"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep4Preview"
import Step1TemplateSelect from "../components/Step1TemplateSelect"
import Step2MaterialSelect from "../components/Step2MaterialSelect"
import Step3GeneratePreview from "../components/Step3GeneratePreview"
import Step4TitleSettings from "../components/Step4TitleSettings"
import Step5VoiceSelect from "../components/Step5VoiceSelect"
import Step3VoiceSelect from "../components/Step5VoiceSelect"
import Step4GeneratePreview from "../components/Step4GeneratePreview"
import Step5TitleSettings from "../components/Step4TitleSettings"
import Step6CoverSettings from "../components/Step6CoverSettings"
import Step7ConfirmGenerate from "../components/Step7ConfirmGenerate"
import type { GeneratedVideo } from "@/api/template-editor"
@@ -63,13 +64,18 @@ export interface GenerateStepContentProps {
/* 其他 */
presetVoices: PresetVoiceItem[]
videoRatio: string
/* Step3 预览 */
previewStatus: PreviewStatus
previewResult: PreviewResult | null
previewError: string
/* Step4 预览(多预览) */
previewCount: number
onPreviewCountChange: (count: number) => void
previewItems: PreviewItem[]
previewSelectedIndex: number
onSelectPreview: (index: number) => void
previewOverallStatus: PreviewStatus
previewOverallError: string
previewOverallProgress: number
previewAnyGenerating: boolean
previewTemplateName: string
previewMaterialCount: string
previewProgress: number
onGeneratePreview: () => void
onRegeneratePreview: () => void
}
@@ -94,14 +100,8 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
selectedVoice,
onSelectedVoiceChange,
voiceMode,
onVoiceModeChange,
selectedClonedVoice,
onSelectedClonedVoiceChange,
clonedVoices,
addClone,
hasProcessing,
cloneModalOpen,
onCloneModalOpenChange,
generateCount,
onGenerateCountChange,
generating,
@@ -113,12 +113,17 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onDismissError,
presetVoices,
videoRatio,
previewStatus,
previewResult,
previewError,
previewCount,
onPreviewCountChange,
previewItems,
previewSelectedIndex,
onSelectPreview,
previewOverallStatus,
previewOverallError,
previewOverallProgress,
previewAnyGenerating,
previewTemplateName,
previewMaterialCount,
previewProgress,
onGeneratePreview,
onRegeneratePreview,
} = props
@@ -145,41 +150,36 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
)
case 3:
return (
<Step3GeneratePreview
templateName={previewTemplateName}
materialCount={previewMaterialCount}
duration={duration}
videoRatio={videoRatio}
previewStatus={previewStatus}
previewResult={previewResult}
previewError={previewError}
progress={previewProgress}
onGeneratePreview={onGeneratePreview}
onRegeneratePreview={onRegeneratePreview}
<Step3VoiceSelect
selectedVoice={selectedVoice}
onSelectedVoiceChange={onSelectedVoiceChange}
/>
)
case 4:
return (
<Step4TitleSettings
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
<Step4GeneratePreview
templateName={previewTemplateName}
materialCount={previewMaterialCount}
duration={duration}
videoRatio={videoRatio}
previewCount={previewCount}
onPreviewCountChange={onPreviewCountChange}
items={previewItems}
selectedIndex={previewSelectedIndex}
onSelectPreview={onSelectPreview}
overallStatus={previewOverallStatus}
overallError={previewOverallError}
overallProgress={previewOverallProgress}
anyGenerating={previewAnyGenerating}
onGeneratePreview={onGeneratePreview}
onRegeneratePreview={onRegeneratePreview}
/>
)
case 5:
return (
<Step5VoiceSelect
selectedVoice={selectedVoice}
onSelectedVoiceChange={onSelectedVoiceChange}
voiceMode={voiceMode}
onVoiceModeChange={onVoiceModeChange}
selectedClonedVoice={selectedClonedVoice}
onSelectedClonedVoiceChange={onSelectedClonedVoiceChange}
clonedVoices={clonedVoices}
addClone={addClone}
hasProcessing={hasProcessing}
cloneModalOpen={cloneModalOpen}
onCloneModalOpenChange={onCloneModalOpenChange}
titleText={titleSettings.title}
<Step5TitleSettings
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
/>
)
case 6:
@@ -188,6 +188,8 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
coverSettings={coverSettings}
onCoverSettingsChange={onCoverSettingsChange}
duration={duration}
assetIds={materialMode === "auto" ? smartSelectedIds : selectedMaterials}
selectedTemplate={selectedTemplate}
/>
)
case 7:
@@ -1,10 +1,20 @@
/**
* 右侧预览视频面板
* Step3 生成预览后常驻显示预览视频
* Step4 生成预览后常驻显示预览视频
* Step5+ 用 Canvas 绘制标题预览(替代 CSS overlay,与 ASS 渲染行为一致)
*
* 设计说明:标题预览仅在有视频时显示(叠加在视频画面上方)。
* 无视频状态(idle/loading/error)下不再单独显示标题预览,这是有意为之的设计简化。
*
* Canvas 居中修复说明:
* Canvas 的 CSS 位置和尺寸直接匹配视频实际渲染区域(通过 getBoundingClientRect),
* 绘制坐标系基于 Canvas 自身尺寸,x = w/2 即可实现水平居中,
* 避免容器与视频尺寸不一致时浏览器拉伸 Canvas 导致居中偏移。
*/
import React from "react"
import React, { useRef, useEffect, useCallback } from "react"
import { PlayCircleOutlined, LoadingOutlined } from "@ant-design/icons"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep4Preview"
import type { TitleSettings } from "../types"
interface PreviewVideoPanelProps {
previewStatus: PreviewStatus
@@ -13,8 +23,138 @@ interface PreviewVideoPanelProps {
progress: number
videoRatio: string
onRegenerate: () => void
/** 标题文字(Step5 起传入) */
titleText?: string
/** 标题样式设置(Step5 起传入) */
titleSettings?: TitleSettings
}
/* ── Canvas 绘制工具函数 ── */
/**
* 将文本按 maxWidth 逐字换行,返回行数组。
* 与 ASS 字幕引擎的逐字换行行为一致。
*/
function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number): string[] {
const lines: string[] = []
let currentLine = ""
for (const char of text) {
const testLine = currentLine + char
if (ctx.measureText(testLine).width > maxWidth && currentLine) {
lines.push(currentLine)
currentLine = char
} else {
currentLine = testLine
}
}
if (currentLine) lines.push(currentLine)
return lines
}
/**
* 在 canvas 上绘制标题文字(含描边/阴影/多行居中)
*
* Canvas 已通过 CSS 定位到视频实际渲染位置,
* 坐标系基于 Canvas 自身尺寸,居中直接使用 w/2。
*
* @param ctx canvas 上下文
* @param w canvas CSS 宽度(= 视频渲染宽度)
* @param h canvas CSS 高度(= 视频渲染高度)
* @param text 标题文字
* @param settings 标题样式
* @param paddingX 左右边距(px),与 ASS 的 MarginL/MarginR 对应
* @param position "top" | "center" | "bottom"
* @param topOffset 顶部/底部偏移量
*/
function drawTitleOnCanvas(
ctx: CanvasRenderingContext2D,
w: number,
h: number,
text: string,
settings: TitleSettings,
paddingX: number,
position: string,
topOffset: number,
) {
const dpr = window.devicePixelRatio || 1
// 设置 canvas 物理像素尺寸(高清屏适配)
ctx.canvas.width = Math.round(w * dpr)
ctx.canvas.height = Math.round(h * dpr)
ctx.scale(dpr, dpr)
// 清除
ctx.clearRect(0, 0, w, h)
// 可用宽度 = 总宽 - 左右边距
const availableWidth = w - paddingX * 2
if (availableWidth <= 0) return
// 字体设置
const fontSize = Math.round(Math.min(settings.size, 36))
const fontWeight = settings.bold ? "bold" : "normal"
const fontStyle = settings.italic ? "italic" : "normal"
ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px "${settings.font}"`
// 文字属性
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.fillStyle = settings.color
const lineHeight = fontSize * 1.4
// 描边 & 阴影
if (settings.stroke) {
ctx.strokeStyle = "rgba(0,0,0,0.6)"
ctx.lineWidth = 2
ctx.lineJoin = "round"
}
if (settings.shadow) {
ctx.shadowColor = "rgba(0,0,0,0.7)"
ctx.shadowBlur = 4
ctx.shadowOffsetX = 2
ctx.shadowOffsetY = 2
}
// 换行
const displayText = text && text.trim() ? text : "请选择或输入标题"
const lines = wrapText(ctx, displayText, availableWidth)
// 起始 Y:根据 position 计算
const totalTextHeight = lines.length * lineHeight
let startY: number
switch (position) {
case "top":
startY = topOffset
break
case "center":
startY = (h - totalTextHeight) / 2 + lineHeight / 2
break
case "bottom":
default:
startY = h - topOffset - totalTextHeight + lineHeight / 2
break
}
// 居中 x = w/2(Canvas 已定位到视频位置,无需额外偏移)
const x = w / 2
lines.forEach((line, i) => {
const y = startY + i * lineHeight
if (settings.stroke) ctx.strokeText(line, x, y)
ctx.fillText(line, x, y)
})
// 重置 shadow(避免影响后续绘制)
if (settings.shadow) {
ctx.shadowColor = "transparent"
ctx.shadowBlur = 0
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 0
}
}
/* ── 组件 ── */
export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
previewStatus,
previewResult,
@@ -22,10 +162,133 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
progress,
videoRatio,
onRegenerate,
titleText,
titleSettings,
}) => {
const hasPreview = previewStatus === "ready" && previewResult
const isLoading = previewStatus === "pending" || previewStatus === "generating"
const isError = previewStatus === "error"
const showTitlePreview = !!titleSettings
const videoAspectStyle = { aspectRatio: (videoRatio || "16:9").replace(":", "/") }
// video 模式 refs
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const videoRef = useRef<HTMLVideoElement>(null)
// 字体加载状态(ref 供 draw 回调同步读取,无需 state 避免触发不必要的重渲染)
const fontLoadedRef = useRef(false)
/** 在 video canvas 上绘制标题 */
const drawVideoTitle = useCallback(() => {
// 通过 ref 读取字体状态,避免 fontLoaded 进入依赖数组
if (!fontLoadedRef.current) return
const canvas = canvasRef.current
const container = containerRef.current
if (!canvas || !container || !titleSettings) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const containerRect = container.getBoundingClientRect()
if (containerRect.width <= 0 || containerRect.height <= 0) return
// 使用 video 元素的 getBoundingClientRect 获取实际渲染尺寸和位置
const videoEl = videoRef.current
let drawW = containerRect.width
let drawH = containerRect.height
let offsetX = 0
let offsetY = 0
if (videoEl && videoEl.clientWidth > 0 && videoEl.clientHeight > 0) {
const videoRect = videoEl.getBoundingClientRect()
drawW = videoRect.width
drawH = videoRect.height
offsetX = videoRect.left - containerRect.left
offsetY = videoRect.top - containerRect.top
}
// 更新 Canvas CSS 位置和尺寸,使其与视频实际渲染区域完全对齐
canvas.style.left = `${offsetX}px`
canvas.style.top = `${offsetY}px`
canvas.style.width = `${drawW}px`
canvas.style.height = `${drawH}px`
// 绘制时,坐标系基于 Canvas 自身尺寸,无需额外偏移
drawTitleOnCanvas(
ctx,
drawW,
drawH,
titleText || "",
titleSettings,
40,
titleSettings.position,
60,
)
}, [titleText, titleSettings])
// video 模式:ResizeObserver 监听容器尺寸变化 → 重绘
useEffect(() => {
if (!showTitlePreview || !hasPreview) return
const container = containerRef.current
if (!container) return
const observer = new ResizeObserver(() => {
drawVideoTitle()
})
observer.observe(container)
requestAnimationFrame(drawVideoTitle)
return () => observer.disconnect()
}, [showTitlePreview, hasPreview, drawVideoTitle])
// 字体加载检测:字体变更时重新检测,确保 measureText 使用正确字体
useEffect(() => {
if (!showTitlePreview || !titleSettings) {
fontLoadedRef.current = false
return
}
let cancelled = false
fontLoadedRef.current = false
const fontWeight = titleSettings.bold ? "bold" : ""
const fontStyle = titleSettings.italic ? "italic" : ""
const fontSpec =
`${fontStyle} ${fontWeight} ${titleSettings.size}px "${titleSettings.font}"`.trim()
const onFontReady = () => {
if (cancelled) return
fontLoadedRef.current = true
// ref 已同步更新,显式触发重绘(draw 内部通过 ref 检查字体状态)
requestAnimationFrame(() => {
if (!cancelled) {
drawVideoTitle()
}
})
}
if (document.fonts.check(fontSpec)) {
onFontReady()
return
}
document.fonts
.load(fontSpec)
.then(() => onFontReady())
.catch(() => {
document.fonts.ready.then(() => onFontReady())
})
return () => {
cancelled = true
}
}, [showTitlePreview, titleSettings, drawVideoTitle])
// video 加载完成后重绘
const handleVideoLoaded = useCallback(() => {
if (showTitlePreview) {
requestAnimationFrame(drawVideoTitle)
}
}, [showTitlePreview, drawVideoTitle])
return (
<div className="xx-generate-preview">
@@ -48,7 +311,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
{/* 生成中 */}
{isLoading && (
<div className="xx-preview-loading-panel">
<div className="xx-preview-video">
<div className="xx-preview-video" style={videoAspectStyle}>
<div className="xx-preview-loading-center">
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
<p style={{ marginTop: 12, color: "rgba(255,255,255,0.8)", fontSize: 14 }}>
@@ -65,7 +328,7 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
{/* 生成失败 */}
{isError && (
<div className="xx-preview-error-panel">
<div className="xx-preview-video xx-preview-video--error">
<div className="xx-preview-video xx-preview-video--error" style={videoAspectStyle}>
<p style={{ color: "rgba(255,255,255,0.8)", fontSize: 14 }}></p>
</div>
<p className="xx-preview-error-msg">
@@ -77,10 +340,32 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
</div>
)}
{/* 预览成功 */}
{/* 预览成功 + Canvas 标题叠加 */}
{hasPreview && (
<div className="xx-preview-video">
<video src={previewResult.videoUrl} controls preload="metadata" />
<div ref={containerRef} style={{ position: "relative" }}>
<div className="xx-preview-video" style={videoAspectStyle}>
<video
ref={videoRef}
src={previewResult.videoUrl}
controls
preload="metadata"
onLoadedMetadata={handleVideoLoaded}
/>
</div>
{showTitlePreview && (
<canvas
ref={canvasRef}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
zIndex: 1,
pointerEvents: "none",
}}
/>
)}
</div>
)}
@@ -65,8 +65,6 @@ const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => {
{m.materialMode === "auto" && (
<div className="xx-smart-match-section">
<SmartMatchInput
inputValue={m.smartMatchInput}
onInputChange={m.setSmartMatchInput}
matching={m.smartMatching}
onMatch={m.handleSmartMatch}
hasMatched={m.hasMatched}
@@ -76,7 +74,7 @@ const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => {
/>
<SmartMatchResults
results={m.smartMatchedResults}
matchedAssets={m.smartMatchedResults}
selectedIds={m.smartSelectedIds}
matching={m.smartMatching}
hasMatched={m.hasMatched}
@@ -1,170 +0,0 @@
/**
* Step 3 生成预览组件
* 调用后端预览生成接口,展示真实视频预览
*/
import React from "react"
import {
CheckCircleFilled,
LoadingOutlined,
ReloadOutlined,
PlayCircleOutlined,
ExclamationCircleFilled,
ClockCircleOutlined,
} from "@ant-design/icons"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep3Preview"
interface Step3GeneratePreviewProps {
templateName: string
materialCount: string
duration: number
videoRatio: string
previewStatus: PreviewStatus
previewResult: PreviewResult | null
previewError: string
progress: number
onGeneratePreview: () => void
onRegeneratePreview: () => void
}
const Step3GeneratePreview: React.FC<Step3GeneratePreviewProps> = ({
templateName,
materialCount,
videoRatio,
previewStatus,
previewResult,
previewError,
progress,
onGeneratePreview,
onRegeneratePreview,
}) => {
return (
<div className="xx-form-section">
<h3>🎬 </h3>
{/* 预览生成按钮 */}
{previewStatus === "idle" && (
<div className="xx-preview-generate-section">
<div className="xx-preview-generate-hint">
<PlayCircleOutlined style={{ fontSize: 32, color: "#3b82f6", marginBottom: 12 }} />
<p className="xx-preview-generate-title"></p>
<p className="xx-preview-generate-desc">
AI 480p
</p>
</div>
<button
className="xx-btn xx-btn-primary xx-preview-generate-btn"
onClick={onGeneratePreview}
>
</button>
</div>
)}
{/* 排队中 */}
{previewStatus === "pending" && (
<div className="xx-preview-loading">
<ClockCircleOutlined style={{ fontSize: 32, color: "#faad14" }} spin />
<p className="xx-preview-loading-text">...</p>
<p className="xx-preview-loading-desc"></p>
</div>
)}
{/* 生成中 */}
{previewStatus === "generating" && (
<div className="xx-preview-loading">
<LoadingOutlined style={{ fontSize: 32, color: "#3b82f6" }} spin />
<p className="xx-preview-loading-text">... {progress}%</p>
<p className="xx-preview-loading-desc">AI </p>
<div className="xx-preview-progress-bar">
<div className="xx-preview-progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
)}
{/* 生成失败 */}
{previewStatus === "error" && (
<div className="xx-preview-error">
<ExclamationCircleFilled style={{ fontSize: 28, color: "#ef4444" }} />
<p className="xx-preview-error-text"></p>
<p className="xx-preview-error-desc">
{typeof previewError === "string" && previewError ? previewError : "请稍后重试"}
</p>
<button className="xx-btn xx-btn-primary" onClick={onRegeneratePreview}>
<ReloadOutlined />
</button>
</div>
)}
{/* 生成成功 - 视频预览 */}
{previewStatus === "ready" && previewResult && (
<>
<div className="xx-preview-tip">
<CheckCircleFilled style={{ color: "#52c41a", marginRight: 8 }} />
<span></span>
<button
className="xx-preview-regenerate-btn"
onClick={onRegeneratePreview}
title="重新生成"
>
<ReloadOutlined />
</button>
</div>
{/* 视频播放器 */}
<div className="xx-preview-video-wrapper">
<video
className="xx-preview-video"
src={previewResult.videoUrl}
controls
preload="metadata"
/>
</div>
{/* 剪辑方案信息 */}
<div className="xx-preview-plan-card">
<div className="xx-preview-plan-title"></div>
<div className="xx-preview-plan-info">
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{templateName}</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{materialCount}</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{previewResult.clipCount} </span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{previewResult.transitionCount} </span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label">使</span>
<span className="xx-preview-plan-value">{previewResult.materialUsage}%</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">
{(typeof previewResult.duration === "number"
? previewResult.duration
: 0
).toFixed(1)}{" "}
</span>
</div>
<div className="xx-preview-plan-row">
<span className="xx-preview-plan-label"></span>
<span className="xx-preview-plan-value">{videoRatio}</span>
</div>
</div>
<div className="xx-preview-plan-hint">💡 480p </div>
</div>
</>
)}
</div>
)
}
export default Step3GeneratePreview
@@ -0,0 +1,294 @@
/**
* Step 4 生成预览组件(支持多预览)
* 调用后端预览生成接口,展示多个真实视频预览(网格布局)
*/
import React from "react"
import {
CheckCircleFilled,
LoadingOutlined,
ReloadOutlined,
PlayCircleOutlined,
ExclamationCircleFilled,
ClockCircleOutlined,
} from "@ant-design/icons"
import { InputNumber } from "antd"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep4Preview"
interface Step4GeneratePreviewProps {
templateName: string
materialCount: string
duration: number
videoRatio: string
previewCount: number
onPreviewCountChange: (count: number) => void
items: PreviewItem[]
selectedIndex: number
onSelectPreview: (index: number) => void
overallStatus: PreviewStatus
overallError: string
overallProgress: number
anyGenerating: boolean
onGeneratePreview: () => void
onRegeneratePreview: () => void
}
/** 预览数量选项 */
const PREVIEW_COUNT_OPTIONS = [
{ value: 1, label: "1个" },
{ value: 2, label: "2个" },
{ value: 3, label: "3个" },
]
const Step4GeneratePreview: React.FC<Step4GeneratePreviewProps> = ({
templateName: _templateName,
materialCount: _materialCount,
videoRatio,
previewCount,
onPreviewCountChange,
items,
selectedIndex,
onSelectPreview,
overallStatus,
overallError,
overallProgress,
anyGenerating,
onGeneratePreview,
onRegeneratePreview,
}) => {
const aspectRatio = (videoRatio || "16:9").replace(":", "/") // "9:16" → "9/16", "16:9" → "16/9"
const isIdle = overallStatus === "idle"
const isError = overallStatus === "error" && !items.some((it) => it.status === "ready")
return (
<div className="xx-form-section">
<h3>🎬 </h3>
{/* 预览数量选择器(仅在 idle 状态显示) */}
{isIdle && (
<div style={{ marginBottom: 16, display: "flex", alignItems: "center", gap: 12 }}>
<span style={{ fontSize: 14, color: "#666" }}></span>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
{PREVIEW_COUNT_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => onPreviewCountChange(opt.value)}
style={{
padding: "4px 12px",
borderRadius: 6,
border: previewCount === opt.value ? "1px solid #1677ff" : "1px solid #d9d9d9",
background: previewCount === opt.value ? "#e6f4ff" : "#fff",
color: previewCount === opt.value ? "#1677ff" : "#666",
cursor: "pointer",
fontSize: 13,
fontWeight: previewCount === opt.value ? 600 : 400,
}}
>
{opt.label}
</button>
))}
<InputNumber
min={1}
max={10}
value={previewCount}
onChange={(val) => val && onPreviewCountChange(val)}
style={{ width: 70 }}
placeholder="自定义"
/>
<span style={{ fontSize: 12, color: "#999", marginLeft: 4 }}>1~10</span>
</div>
{previewCount > 1 && (
<span style={{ fontSize: 12, color: "#999" }}></span>
)}
</div>
)}
{/* 预览生成按钮(idle 状态) */}
{isIdle && (
<div className="xx-preview-generate-section">
<div className="xx-preview-generate-hint">
<PlayCircleOutlined style={{ fontSize: 32, color: "#3b82f6", marginBottom: 12 }} />
<p className="xx-preview-generate-title"></p>
<p className="xx-preview-generate-desc">
AI
{previewCount > 1 ? `${previewCount}个不同版本的` : ""}480p
</p>
</div>
<button
className="xx-btn xx-btn-primary xx-preview-generate-btn"
onClick={onGeneratePreview}
>
{previewCount > 1 ? `${previewCount}个)` : ""}
</button>
</div>
)}
{/* 整体排队中(所有都在 pending) */}
{anyGenerating && items.every((it) => it.status === "pending") && (
<div className="xx-preview-loading">
<ClockCircleOutlined style={{ fontSize: 32, color: "#faad14" }} spin />
<p className="xx-preview-loading-text">...</p>
<p className="xx-preview-loading-desc"></p>
</div>
)}
{/* 多预览网格(生成中/完成/部分完成) */}
{(anyGenerating || overallStatus === "ready") && items.length > 0 && (
<div
className="xx-preview-grid"
style={{
display: "grid",
gridTemplateColumns: `repeat(${Math.min(items.length, 3)}, 1fr)`,
gap: 12,
maxWidth: `${Math.min(items.length, 3) * 280 + (Math.min(items.length, 3) - 1) * 12}px`,
margin: "0 auto 16px",
}}
>
{items.map((item) => {
const isSelected = item.index === selectedIndex
return (
<div
key={item.index}
onClick={() => {
if (item.status === "ready") onSelectPreview(item.index)
}}
style={{
borderRadius: 8,
border: isSelected ? "2px solid #1677ff" : "1px solid #e8e8e8",
overflow: "hidden",
cursor: item.status === "ready" ? "pointer" : "default",
opacity: item.status === "error" ? 0.6 : 1,
transition: "all 0.2s",
}}
>
{/* 缩略图/状态区域 */}
<div
style={{
aspectRatio,
background: "#000",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
}}
>
{item.status === "ready" && item.result && (
<video
src={item.result.videoUrl}
controls
autoPlay
muted
loop
preload="metadata"
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
/>
)}
{(item.status === "pending" || item.status === "generating") && (
<div style={{ textAlign: "center" }}>
<LoadingOutlined style={{ fontSize: 24, color: "#fff" }} spin />
<p style={{ color: "rgba(255,255,255,0.8)", fontSize: 12, marginTop: 8 }}>
{item.status === "pending" ? "排队中..." : `生成中 ${item.progress}%`}
</p>
</div>
)}
{item.status === "error" && (
<div style={{ textAlign: "center", padding: 8 }}>
<ExclamationCircleFilled style={{ fontSize: 20, color: "#ef4444" }} />
<p
style={{
color: "rgba(255,255,255,0.7)",
fontSize: 11,
marginTop: 4,
}}
>
</p>
</div>
)}
{/* 选中角标 */}
{isSelected && item.status === "ready" && (
<div
style={{
position: "absolute",
top: 4,
right: 4,
background: "#1677ff",
color: "#fff",
fontSize: 10,
padding: "2px 6px",
borderRadius: 4,
}}
>
#{item.index + 1}
</div>
)}
</div>
{/* 底部信息 */}
{item.status === "ready" && item.result && (
<div
style={{
padding: "6px 8px",
background: "#fafafa",
fontSize: 11,
color: "#666",
display: "flex",
justifyContent: "space-between",
}}
>
<span>{item.result.duration.toFixed(1)}</span>
<span>{item.result.clipCount}</span>
</div>
)}
</div>
)
})}
</div>
)}
{/* 整体进度条(多预览生成中) */}
{anyGenerating && (
<div className="xx-preview-progress-bar" style={{ marginBottom: 12 }}>
<div className="xx-preview-progress-fill" style={{ width: `${overallProgress}%` }} />
</div>
)}
{/* 全部完成提示 */}
{overallStatus === "ready" && (
<div className="xx-preview-tip">
<CheckCircleFilled style={{ color: "#52c41a", marginRight: 8 }} />
<span>
{items.filter((it) => it.status === "ready").length}
{items.length > 1 ? ",点击选择要查看的版本" : ",确认效果后进入下一步"}
</span>
<button
className="xx-preview-regenerate-btn"
onClick={onRegeneratePreview}
title="重新生成"
>
<ReloadOutlined />
</button>
</div>
)}
{/* 全部失败 */}
{isError && (
<div className="xx-preview-error">
<ExclamationCircleFilled style={{ fontSize: 28, color: "#ef4444" }} />
<p className="xx-preview-error-text"></p>
<p className="xx-preview-error-desc">
{typeof overallError === "string" && overallError ? overallError : "请稍后重试"}
</p>
<button className="xx-btn xx-btn-primary" onClick={onRegeneratePreview}>
<ReloadOutlined />
</button>
</div>
)}
</div>
)
}
export default Step4GeneratePreview
@@ -2,7 +2,7 @@
* Step 4 标题设置组件
*/
import React from "react"
import { Select } from "antd"
import { AutoComplete } from "antd"
import { POSITION_OPTIONS, FONT_OPTIONS } from "../constants"
import type { TitleSettings } from "../types"
import { useStep4Title } from "../hooks/useStep4Title"
@@ -21,40 +21,73 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
<div className="xx-form-section">
<h3>📝 </h3>
<AiTitleGenerator
inputValue={t.aiTitleInput}
onInputChange={t.setAiTitleInput}
generating={t.aiTitleGenerating}
onGenerate={t.handleGenerateAiTitles}
results={t.aiTitleResults}
hasGenerated={t.hasGeneratedTitles}
onSelect={t.handleSelectAiTitle}
selectedTitle={t.titleSettings.title}
onRefresh={t.handleRefreshAiTitles}
/>
{/* AI 自动选择模式 */}
{t.titleSettings.aiAutoSelect && (
<>
<div className="xx-title-ai-toggle">
<span className="xx-toggle-label">AI </span>
<div className="xx-switch active" onClick={t.toggleAiAutoSelect}>
<div className="xx-switch-knob" />
</div>
</div>
<div className="xx-divider">
<span></span>
</div>
<div className="xx-title-ai-toggle">
<span className="xx-toggle-label">AI </span>
<div
className={`xx-switch ${t.titleSettings.aiAutoSelect ? "active" : ""}`}
onClick={t.toggleAiAutoSelect}
>
<div className="xx-switch-knob" />
</div>
</div>
{/* 显示当前 AI 选中的标题(只读)+ 换一个按钮 */}
<div className="xx-form-field">
<label> AI </label>
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 12px",
background: "var(--bg-secondary, rgba(0,0,0,0.04))",
borderRadius: 8,
fontSize: 14,
color: "var(--text-primary, #333)",
}}
>
<span style={{ flex: 1 }}>{t.titleSettings.title || "AI 将自动为你选择标题"}</span>
<button
type="button"
className="xx-btn xx-btn-primary"
style={{ flexShrink: 0, fontSize: 13, padding: "4px 12px" }}
onClick={t.autoGenerateTitle}
>
🔄
</button>
</div>
</div>
</>
)}
{/* 手动选择模式 */}
{!t.titleSettings.aiAutoSelect && (
<>
<AiTitleGenerator
inputValue={t.aiTitleInput}
onInputChange={t.setAiTitleInput}
generating={t.aiTitleGenerating}
onGenerate={t.handleGenerateAiTitles}
results={t.aiTitleResults}
hasGenerated={t.hasGeneratedTitles}
onSelect={t.handleSelectAiTitle}
selectedTitle={t.titleSettings.title}
onRefresh={t.handleRefreshAiTitles}
/>
<div className="xx-title-ai-toggle">
<span className="xx-toggle-label">AI </span>
<div className="xx-switch" onClick={t.toggleAiAutoSelect}>
<div className="xx-switch-knob" />
</div>
</div>
<div className="xx-form-field">
<label></label>
<Select
placeholder="请选择标题…"
<label></label>
<AutoComplete
placeholder="输入或从标题库选择…"
allowClear
showSearch
maxLength={50}
style={{ width: "100%" }}
value={t.titleSettings.title || undefined}
onChange={(val) => t.updateTitle(val || "")}
@@ -62,9 +95,10 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
label: ut.content,
value: ut.content,
}))}
filterOption={(input, option) =>
((option?.label as string) || "").toLowerCase().includes(input.toLowerCase())
}
filterOption={(inputValue, option) => {
const title = (option?.label || option?.value || "") as string
return title.toLowerCase().includes((inputValue || "").toLowerCase())
}}
notFoundContent={
t.userTitles.length === 0 ? (
<span style={{ color: "var(--text-tertiary)", fontSize: 13 }}>
@@ -74,33 +108,25 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
}
/>
</div>
<div className="xx-form-field" style={{ marginTop: 14 }}>
<label></label>
<input
placeholder="输入自定义标题…"
value={t.titleSettings.title}
onChange={(e) => t.updateTitle(e.target.value)}
maxLength={50}
/>
</div>
<TitleStylePanel
settings={t.titleSettings}
onUpdatePosition={t.updatePosition}
onUpdateFont={t.updateFont}
onUpdateSize={t.updateSize}
onToggleBold={t.toggleBold}
onToggleItalic={t.toggleItalic}
onToggleStroke={t.toggleStroke}
onToggleShadow={t.toggleShadow}
onApplyPreset={t.applyPreset}
activePreset={t.activePreset}
titlePresets={t.titlePresets}
POSITION_OPTIONS={POSITION_OPTIONS}
FONT_OPTIONS={FONT_OPTIONS}
/>
</>
)}
{/* 样式面板始终可见,两种模式下都可调整标题展示样式 */}
<TitleStylePanel
settings={t.titleSettings}
onUpdatePosition={t.updatePosition}
onUpdateFont={t.updateFont}
onUpdateSize={t.updateSize}
onToggleBold={t.toggleBold}
onToggleItalic={t.toggleItalic}
onToggleStroke={t.toggleStroke}
onToggleShadow={t.toggleShadow}
onApplyPreset={t.applyPreset}
activePreset={t.activePreset}
titlePresets={t.titlePresets}
POSITION_OPTIONS={POSITION_OPTIONS}
FONT_OPTIONS={FONT_OPTIONS}
/>
</div>
)
}
@@ -1,137 +1,252 @@
/**
* Step 5 配音选择组件
* 展示用户已上传的配音素材,支持选中、预览播放
*/
import React from "react"
import type { VoiceClone } from "@/api/voice-clone"
import { useStep5Voice } from "../hooks/useStep5Voice"
import VoiceRecommendSection from "./voice/VoiceRecommendSection"
import VoiceChoiceCard from "./voice/VoiceChoiceCard"
import PresetVoiceDetail from "./voice/PresetVoiceDetail"
import CustomVoicePanel from "./voice/CustomVoicePanel"
import SaveVoiceModal from "./voice/SaveVoiceModal"
import CloneVoiceSection from "./voice/CloneVoiceSection"
import React, { useState, useRef, useCallback } from "react"
import { useNavigate } from "react-router-dom"
import { useQuery } from "@tanstack/react-query"
import { AudioOutlined, SoundOutlined } from "@ant-design/icons"
import { getAssetsByKind } from "@/api/assets"
import type { AssetItem } from "@/api/assets"
interface Step5VoiceSelectProps {
selectedVoice: string
onSelectedVoiceChange: (voiceId: string) => void
voiceMode: "preset" | "custom" | "clone"
onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void
selectedClonedVoice: string
onSelectedClonedVoiceChange: (voiceId: string) => void
clonedVoices: VoiceClone[]
addClone: (voice: VoiceClone) => void
hasProcessing: boolean
cloneModalOpen: boolean
onCloneModalOpenChange: (open: boolean) => void
titleText: string
onSelectedVoiceChange: (id: string) => void
}
const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = (props) => {
const v = useStep5Voice(props)
/** 格式化时长 mm:ss */
const formatDuration = (seconds?: number): string => {
if (!seconds || seconds <= 0) return "00:00"
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`
}
/** 格式化文件大小 */
const formatFileSize = (bytes?: number): string => {
if (!bytes || bytes <= 0) return "未知"
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
}
const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
selectedVoice,
onSelectedVoiceChange,
}) => {
const navigate = useNavigate()
const [playingId, setPlayingId] = useState<string | null>(null)
const audioRef = useRef<HTMLAudioElement | null>(null)
// 获取用户上传的配音素材
const { data: materials = [], isLoading } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
/** 切换播放/暂停 */
const togglePlay = useCallback(
(material: AssetItem) => {
// 如果当前正在播放同一个素材,则暂停
if (playingId === material.id && audioRef.current) {
audioRef.current.pause()
setPlayingId(null)
return
}
// 停止之前的播放
if (audioRef.current) {
audioRef.current.pause()
}
// 创建新的 audio 元素播放
const audio = new Audio(material.file_url)
audioRef.current = audio
setPlayingId(material.id)
audio.onended = () => {
setPlayingId(null)
audioRef.current = null
}
audio.play().catch(() => {
setPlayingId(null)
audioRef.current = null
})
},
[playingId],
)
/** 选中素材 */
const handleSelect = useCallback(
(id: string) => {
onSelectedVoiceChange(id)
},
[onSelectedVoiceChange],
)
/** 跳转到配音库上传 */
const handleGoToUpload = useCallback(() => {
navigate("/app/voices")
}, [navigate])
// 加载中状态
if (isLoading) {
return (
<div className="xx-form-section">
<h3>🎙 </h3>
<div style={{ textAlign: "center", padding: "40px 0", color: "#999" }}>
...
</div>
</div>
)
}
// 空状态
if (materials.length === 0) {
return (
<div className="xx-form-section">
<h3>🎙 </h3>
<div
style={{
textAlign: "center",
padding: "60px 0",
color: "#999",
}}
>
<AudioOutlined style={{ fontSize: 48, color: "#d9d9d9", marginBottom: 16 }} />
<p style={{ fontSize: 16, marginBottom: 16 }}></p>
<button
type="button"
onClick={handleGoToUpload}
style={{
padding: "8px 24px",
background: "#1677ff",
color: "#fff",
border: "none",
borderRadius: 6,
cursor: "pointer",
fontSize: 14,
}}
>
</button>
</div>
</div>
)
}
return (
<div className="xx-form-section">
<h3>🎙 </h3>
<p style={{ color: "#666", marginBottom: 16, fontSize: 14 }}>
</p>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))",
gap: 12,
}}
>
{materials.map((item) => {
const isSelected = selectedVoice === item.id
const isPlaying = playingId === item.id
<VoiceRecommendSection
presetVoices={v.presetVoices}
voiceRecommendLoading={v.voiceRecommendLoading}
voiceRecommendations={v.voiceRecommendations}
hasVoiceRecommend={v.hasVoiceRecommend}
onRecommend={v.handleVoiceRecommend}
onSelectVoice={v.handleSelectRecommendedVoice}
selectedVoiceId={v.selectedVoice}
voiceMode={v.voiceMode}
VOICE_GENDER_ICON={v.VOICE_GENDER_ICON}
/>
return (
<div
key={item.id}
onClick={() => handleSelect(item.id)}
style={{
padding: 16,
borderRadius: 8,
border: isSelected ? "2px solid #1677ff" : "1px solid #e8e8e8",
background: isSelected ? "#e6f4ff" : "#fff",
cursor: "pointer",
transition: "all 0.2s",
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
{/* 顶部:图标 + 播放按钮 */}
<div
style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}
>
<div
style={{
width: 40,
height: 40,
borderRadius: 8,
background: isSelected
? "linear-gradient(135deg, #1677ff, #4096ff)"
: "linear-gradient(135deg, #f0f0f0, #e8e8e8)",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<AudioOutlined style={{ fontSize: 20, color: isSelected ? "#fff" : "#666" }} />
</div>
{item.file_url && (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
togglePlay(item)
}}
style={{
width: 32,
height: 32,
borderRadius: "50%",
border: "none",
background: isPlaying ? "#ff4d4f" : "#1677ff",
color: "#fff",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 14,
}}
title={isPlaying ? "暂停" : "播放"}
>
<SoundOutlined />
</button>
)}
</div>
<div className="xx-divider">
<span></span>
{/* 名称 */}
<div
style={{
fontSize: 14,
fontWeight: 500,
color: "#333",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
title={item.name}
>
{item.name}
</div>
{/* 时长 + 大小 */}
<div
style={{
display: "flex",
justifyContent: "space-between",
fontSize: 12,
color: "#999",
}}
>
<span>{formatDuration(item.duration)}</span>
<span>{formatFileSize(item.file_size)}</span>
</div>
</div>
)
})}
</div>
<div className="xx-voice-choice-list" style={{ marginBottom: 16 }}>
{v.presetVoices.slice(0, 3).map((pv) => (
<VoiceChoiceCard
key={pv.voice_id}
selected={v.voiceMode === "preset" && v.selectedVoice === pv.voice_id}
onClick={() => v.handleSelectPresetVoice(pv.voice_id)}
avatar={v.VOICE_GENDER_ICON[pv.gender] ?? "✨"}
title={pv.name}
description={pv.description}
/>
))}
{v.presetVoicesLoading && (
<VoiceChoiceCard
selected={false}
onClick={() => {}}
avatar="⏳"
title="加载中…"
loading
/>
)}
<VoiceChoiceCard
selected={v.voiceMode === "clone"}
onClick={v.handleSelectCloneVoice}
avatar="🎤"
title="克隆我的声音"
description="上传语音样本克隆"
avatarStyle={{ background: "linear-gradient(135deg, #10b981, #059669)" }}
/>
</div>
{v.voiceMode === "preset" && (
<PresetVoiceDetail
presetVoices={v.presetVoices}
selectedVoice={v.selectedVoice}
onSelect={v.handleSelectPresetVoice}
playingVoice={v.playingVoice}
onTogglePlay={v.toggleVoicePlay}
presetVoicesLoading={v.presetVoicesLoading}
/>
)}
{v.voiceMode === "custom" && (
<>
<CustomVoicePanel
customVoiceText={v.customVoiceText}
onTextChange={v.setCustomVoiceText}
synthesizePending={v.synthesizeMutation.isPending}
onSynthesize={v.handleSynthesizeVoice}
ttsError={v.ttsError}
customAudioUrl={v.customAudioUrl}
completedTtsJobId={v.completedTtsJobId}
onOpenSaveModal={v.handleOpenSaveModal}
/>
<SaveVoiceModal
open={v.saveModalOpen}
onClose={() => v.setSaveModalOpen(false)}
saveName={v.saveName}
onNameChange={v.setSaveName}
saveTagIds={v.saveTagIds}
onTagIdsChange={v.setSaveTagIds}
saveNewTag={v.saveNewTag}
onNewTagChange={v.setSaveNewTag}
onAddTag={v.handleAddTagInModal}
allTags={v.allTags}
savePending={v.saveToLibraryMutation.isPending}
onConfirm={v.handleConfirmSave}
/>
</>
)}
{v.voiceMode === "clone" && (
<CloneVoiceSection
clonedVoices={v.clonedVoices}
hasProcessing={v.hasProcessing}
selectedClonedVoice={v.selectedClonedVoice}
onSelect={v.handleSelectClonedVoice}
onOpenCloneModal={v.handleOpenCloneModal}
playingVoiceId={v.playingCloneVoice}
onPlaySample={v.handlePlayCloneSample}
CLONE_STATUS_CONFIG={v.CLONE_STATUS_CONFIG}
formatDuration={v.formatDuration}
/>
)}
</div>
)
}
@@ -1,4 +1,4 @@
import React from "react"
import React, { useEffect, useRef } from "react"
import type { CoverConfig } from "../../editing-planner/types"
import { useStep6Cover } from "../hooks/useStep6Cover"
import { CoverModeSelector } from "./cover-settings/CoverModeSelector"
@@ -9,6 +9,10 @@ interface Step6CoverSettingsProps {
coverSettings: CoverConfig
onCoverSettingsChange: (settings: CoverConfig) => void
duration: number
/** 当前素材 ID 列表,用于智能封面生成 */
assetIds?: string[]
/** 当前选中的模板 ID */
selectedTemplate?: string
}
const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
@@ -19,10 +23,47 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
setMode,
setFrameTime,
handleUpload,
generateAutoCover,
totalDuration,
COVER_MODE_LABELS,
COVER_MODE_ICONS,
} = useStep6Cover(props)
} = useStep6Cover({
coverSettings: props.coverSettings,
onCoverSettingsChange: props.onCoverSettingsChange,
duration: props.duration,
assetIds: props.assetIds,
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,
])
// 预览图:优先 thumbnail_url,其次 upload_url
const previewUrl = coverSettings.thumbnail_url || coverSettings.upload_url
return (
<div className="xx-form-section">
@@ -52,9 +93,6 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
{coverSettings.mode === "auto" && (
<div className="xx-cover-auto">
<div className="xx-cover-auto-desc">
AI
</div>
<div className="xx-cover-auto-badge">
<span style={{ fontSize: 24 }}>🤖</span>
<span>AI </span>
@@ -77,21 +115,21 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<div className="xx-section-title"></div>
<div className="xx-cover-preview-box">
{coverSettings.upload_url ? (
<img src={coverSettings.upload_url} alt="封面预览" className="xx-cover-preview-img" />
{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 智能选择"
? "AI 正在选择..."
: coverSettings.mode === "frame"
? `${formatTime(coverSettings.frame_time)}`
: "未上传封面"}
</span>
</div>
)}
<div className="xx-cover-preview-ratio">16:9</div>
<div className="xx-cover-preview-ratio">9:16</div>
</div>
</>
)}
@@ -1,7 +1,7 @@
/**
* 手动选择素材列表
*/
import React from "react"
import React, { useRef, useCallback } from "react"
import { Typography } from "antd"
import type { AssetItem } from "@/api/assets"
@@ -20,6 +20,33 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
selectedMaterials,
onToggle,
}) => {
// 追踪当前正在播放的视频元素,确保同时只有一个视频播放
const activeVideoRef = useRef<HTMLVideoElement | null>(null)
const handleVideoMouseEnter = useCallback((e: React.MouseEvent<HTMLVideoElement>) => {
const video = e.currentTarget
// 暂停之前正在播放的视频(检查是否仍在 DOM 中)
if (
activeVideoRef.current &&
activeVideoRef.current !== video &&
document.body.contains(activeVideoRef.current)
) {
activeVideoRef.current.pause()
activeVideoRef.current.currentTime = 0
}
activeVideoRef.current = video
video.play().catch(() => {})
}, [])
const handleVideoMouseLeave = useCallback((e: React.MouseEvent<HTMLVideoElement>) => {
const video = e.currentTarget
video.pause()
video.currentTime = 0
if (activeVideoRef.current === video) {
activeVideoRef.current = null
}
}, [])
return (
<div style={{ marginTop: 14 }}>
{materialsLoading ? (
@@ -32,6 +59,8 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
{materials.items.map((m) => {
const checked = selectedMaterials.includes(m.id)
const isVideo = m.mime_type?.startsWith("video/") ?? false
const thumbSrc = m.thumbnail_url || undefined
return (
<label
key={m.id}
@@ -53,13 +82,103 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
type="checkbox"
checked={checked}
onChange={() => onToggle(m.id)}
style={{ accentColor: "var(--primary-color, #4f46e5)" }}
style={{
accentColor: "var(--primary-color, #4f46e5)",
flexShrink: 0,
}}
/>
{/* 缩略图预览 48×48 */}
<div
style={{
width: 48,
height: 48,
borderRadius: 6,
overflow: "hidden",
background: "#e2e8f0",
flexShrink: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{isVideo && m.file_url ? (
<video
src={m.file_url}
poster={m.thumbnail_url || undefined}
muted
loop
playsInline
preload="none"
onMouseEnter={handleVideoMouseEnter}
onMouseLeave={handleVideoMouseLeave}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
cursor: "pointer",
}}
/>
) : thumbSrc ? (
<img
src={thumbSrc}
alt={m.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.nextElementSibling as HTMLElement | null
if (fallback) fallback.style.display = "flex"
}}
/>
) : null}
{!thumbSrc && !isVideo && (
<span
style={{
fontSize: 20,
opacity: 0.5,
display: "flex",
}}
>
🎵
</span>
)}
{!thumbSrc && isVideo && !m.file_url && (
<span
style={{
fontSize: 20,
opacity: 0.5,
display: "flex",
}}
>
🎬
</span>
)}
{/* img onError 时显示的 fallback(初始隐藏) */}
{thumbSrc && !(isVideo && m.file_url) && (
<span
style={{
fontSize: 20,
opacity: 0.5,
display: "none",
}}
>
{isVideo ? "🎬" : "🎵"}
</span>
)}
</div>
<span
style={{
fontSize: 13,
color: "var(--text-primary)",
flex: 1,
minWidth: 0,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{m.name}
@@ -68,9 +187,10 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
style={{
fontSize: 11,
color: "var(--text-tertiary, #94a3b8)",
flexShrink: 0,
}}
>
{m.mime_type.split("/")[1].toUpperCase()}
{m.mime_type?.split("/")?.[1]?.toUpperCase() ?? "FILE"}
</span>
</label>
)
@@ -1,30 +1,24 @@
/**
* 单个智能匹配卡片
* 智能匹配卡片Q5 简化版)
* 只展示素材缩略图、名称、时长,无匹配分数
*/
import React from "react"
import { PlayCircleOutlined, CheckCircleFilled } from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
interface SmartMatchResultItem {
asset: AssetItem
matchScore: number
matchReason: string
}
interface SmartMatchCardProps {
result: SmartMatchResultItem
asset: AssetItem
selected: boolean
onClick: () => void
formatDuration: (seconds: number) => string
}
const SmartMatchCard: React.FC<SmartMatchCardProps> = ({
result,
asset,
selected,
onClick,
formatDuration,
}) => {
const { asset, matchScore, matchReason } = result
return (
<div className={`xx-smart-match-card ${selected ? "selected" : ""}`} onClick={onClick}>
{/* 缩略图 */}
@@ -36,7 +30,6 @@ const SmartMatchCard: React.FC<SmartMatchCardProps> = ({
<PlayCircleOutlined style={{ fontSize: 32, opacity: 0.5 }} />
</div>
)}
<div className="xx-smart-match-score">{matchScore}%</div>
{selected && (
<div className="xx-smart-match-check">
<CheckCircleFilled style={{ fontSize: 20, color: "#fff" }} />
@@ -46,12 +39,11 @@ const SmartMatchCard: React.FC<SmartMatchCardProps> = ({
<div className="xx-smart-match-duration">{formatDuration(asset.duration)}</div>
)}
</div>
{/* 信息区 */}
{/* 名称 */}
<div className="xx-smart-match-info">
<div className="xx-smart-match-name" title={asset.name}>
{asset.name}
</div>
<div className="xx-smart-match-reason">🎯 {matchReason}</div>
</div>
</div>
)
@@ -1,13 +1,11 @@
/**
* 智能匹配输入区
* textarea + 提示 + 按钮组
* 智能匹配操作区(Q5 简化版)
* 一键触发 AI 选素材,无需输入描述
*/
import React from "react"
import { LoadingOutlined } from "@ant-design/icons"
import { LoadingOutlined, ThunderboltOutlined } from "@ant-design/icons"
interface SmartMatchInputProps {
inputValue: string
onInputChange: (value: string) => void
matching: boolean
onMatch: () => void
hasMatched: boolean
@@ -17,8 +15,6 @@ interface SmartMatchInputProps {
}
const SmartMatchInput: React.FC<SmartMatchInputProps> = ({
inputValue,
onInputChange,
matching,
onMatch,
hasMatched,
@@ -28,22 +24,9 @@ const SmartMatchInput: React.FC<SmartMatchInputProps> = ({
}) => {
return (
<div className="xx-smart-match-input-area">
<label className="xx-smart-match-label">🤖 </label>
<textarea
className="xx-smart-match-input"
placeholder="例如:一个科技感十足的产品宣传视频,画面要有现代办公场景、团队协作、数据分析图表…"
value={inputValue}
onChange={(e) => onInputChange(e.target.value)}
rows={3}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
onMatch()
}
}}
/>
<div className="xx-smart-match-input-footer">
<div className="xx-smart-match-action-row">
<span className="xx-smart-match-tip">
{loading ? "扫描视频库中…" : `当前视频库共 ${materialsCount} 个素材可供匹配`}
{loading ? "扫描视频库中…" : `当前视频库共 ${materialsCount} 个素材`}
</span>
<div style={{ display: "flex", gap: 8 }}>
{hasMatched && (
@@ -60,15 +43,18 @@ const SmartMatchInput: React.FC<SmartMatchInputProps> = ({
type="button"
className="xx-btn xx-btn-primary xx-btn-sm"
onClick={onMatch}
disabled={matching || loading || !inputValue.trim()}
disabled={matching || loading}
>
{matching ? (
<>
<LoadingOutlined style={{ marginRight: 6 }} />
AI
</>
) : (
"✨ 智能匹配"
<>
<ThunderboltOutlined style={{ marginRight: 6 }} />
{hasMatched ? "重新选择" : "让 AI 帮你选"}
</>
)}
</button>
</div>
@@ -1,19 +1,14 @@
/**
* 智能匹配结果区(含加载/空状态/已选汇总
* 智能匹配结果区(Q5 简化版
* 直接展示 AI 选中的素材,无匹配分数和理由
*/
import React from "react"
import { LoadingOutlined } from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
import SmartMatchCard from "./SmartMatchCard"
interface SmartMatchResultItem {
asset: AssetItem
matchScore: number
matchReason: string
}
interface SmartMatchResultsProps {
results: SmartMatchResultItem[]
matchedAssets: AssetItem[]
selectedIds: string[]
matching: boolean
hasMatched: boolean
@@ -25,7 +20,7 @@ interface SmartMatchResultsProps {
}
const SmartMatchResults: React.FC<SmartMatchResultsProps> = ({
results,
matchedAssets,
selectedIds,
matching,
hasMatched,
@@ -42,9 +37,9 @@ const SmartMatchResults: React.FC<SmartMatchResultsProps> = ({
<LoadingOutlined
style={{ fontSize: 32, color: "var(--primary-color)", marginBottom: 12 }}
/>
<div style={{ color: "var(--text-primary)", fontSize: 14 }}>AI </div>
<div style={{ color: "var(--text-primary)", fontSize: 14 }}>AI </div>
<div style={{ color: "var(--text-tertiary)", fontSize: 12, marginTop: 4 }}>
</div>
</div>
)
@@ -56,20 +51,22 @@ const SmartMatchResults: React.FC<SmartMatchResultsProps> = ({
<div className="xx-smart-match-empty">
<div style={{ fontSize: 36, marginBottom: 8 }}>💡</div>
<div style={{ color: "var(--text-secondary)", fontSize: 13 }}>
AI
AI
</div>
</div>
)
}
// 无结果
if (results.length === 0) return null
if (matchedAssets.length === 0) return null
return (
<>
<div className="xx-smart-match-results">
<div className="xx-smart-match-results-header">
<span className="xx-smart-match-results-title"> ({results.length})</span>
<span className="xx-smart-match-results-title">
AI ({matchedAssets.length})
</span>
<div className="xx-smart-match-results-actions">
<button type="button" className="xx-link-btn" onClick={onSelectAll}>
@@ -81,14 +78,14 @@ const SmartMatchResults: React.FC<SmartMatchResultsProps> = ({
</div>
</div>
<div className="xx-smart-match-grid">
{results.map((result) => {
const isSelected = selectedIds.includes(result.asset.id)
{matchedAssets.map((asset) => {
const isSelected = selectedIds.includes(asset.id)
return (
<SmartMatchCard
key={result.asset.id}
result={result}
key={asset.id}
asset={asset}
selected={isSelected}
onClick={() => onToggle(result.asset.id)}
onClick={() => onToggle(asset.id)}
formatDuration={formatDuration}
/>
)
@@ -13,9 +13,15 @@ interface TitlePresetsGridProps {
presets: TitlePresetItem[]
activePreset: string | null
onApply: (presetKey: string) => void
fontFamily?: string
}
const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({ presets, activePreset, onApply }) => {
const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({
presets,
activePreset,
onApply,
fontFamily,
}) => {
return (
<div className="xx-title-presets-grid">
{presets.map((p) => {
@@ -27,7 +33,10 @@ const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({ presets, activePres
onClick={() => onApply(p.key)}
title={p.label}
>
<span className="xx-title-preset-preview-text" style={p.previewStyle}>
<span
className="xx-title-preset-preview-text"
style={{ ...p.previewStyle, ...(fontFamily ? { fontFamily } : {}) }}
>
</span>
<span className="xx-title-preset-card-label">{p.label}</span>
@@ -107,6 +107,7 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
presets={titlePresets}
activePreset={activePreset}
onApply={onApplyPreset}
fontFamily={settings.font}
/>
</div>
+3 -3
View File
@@ -31,9 +31,9 @@ export const VOICE_GENDER_ICON: Record<string, string> = {
export const STEPS = [
{ key: 1, label: "选择模板" },
{ key: 2, label: "选择素材" },
{ key: 3, label: "生成预览" },
{ key: 4, label: "选择标题" },
{ key: 5, label: "选择配音" },
{ key: 3, label: "选择配音" },
{ key: 4, label: "生成预览" },
{ key: 5, label: "选择标题" },
{ key: 6, label: "选择封面" },
{ key: 7, label: "确认生成" },
]
+26 -39
View File
@@ -891,7 +891,7 @@
/* ── 视频预览 ── */
.xx-preview-video {
aspect-ratio: 9 / 16;
aspect-ratio: 16 / 9;
max-height: 400px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, var(--color-gray-900), var(--color-primary-900));
@@ -1433,47 +1433,14 @@
border-radius: 14px;
}
.xx-smart-match-label {
display: block;
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #1e293b);
margin-bottom: 10px;
}
.xx-smart-match-input {
width: 100%;
min-height: 80px;
padding: 12px;
font-size: 13px;
line-height: 1.6;
color: var(--text-primary, #1e293b);
background: #fff;
border: 1px solid var(--border-primary, #e2e8f0);
border-radius: 10px;
resize: vertical;
box-sizing: border-box;
transition: border-color 0.2s;
font-family: inherit;
}
.xx-smart-match-input:focus {
outline: none;
border-color: var(--primary-color, #4f46e5);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
}
.xx-smart-match-input::placeholder {
color: var(--text-tertiary, #94a3b8);
}
.xx-smart-match-input-footer {
.xx-smart-match-action-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
/* textarea removed in Q5 */
.xx-smart-match-tip {
font-size: 12px;
color: var(--text-tertiary, #94a3b8);
@@ -2389,6 +2356,8 @@
background: var(--bg-tertiary);
border-radius: var(--radius-lg);
border: 2px dashed var(--border-color);
max-width: 320px;
margin: 0 auto;
}
.xx-preview-generate-hint {
@@ -2414,6 +2383,7 @@
font-size: 14px;
}
/* ── 加载中状态 ── */
.xx-preview-loading {
text-align: center;
@@ -2421,6 +2391,8 @@
background: var(--bg-tertiary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
max-width: 320px;
margin: 0 auto;
}
.xx-preview-loading-text {
@@ -2436,13 +2408,15 @@
margin: 0;
}
/* ── 错误状态 ── */
/* ── 错误状态(手机屏尺寸)── */
.xx-preview-error {
text-align: center;
padding: 32px 20px;
background: rgba(239, 68, 68, 0.06);
border-radius: var(--radius-lg);
border: 1px solid rgba(239, 68, 68, 0.2);
max-width: 320px;
margin: 0 auto;
}
.xx-preview-error-text {
@@ -2489,6 +2463,9 @@
margin-bottom: 20px;
font-size: 13px;
color: var(--text-primary);
max-width: 320px;
margin-left: auto;
margin-right: auto;
}
.xx-preview-plan-card {
@@ -2603,6 +2580,16 @@
z-index: 2;
}
/* ── 整体进度条(手机屏尺寸)── */
.xx-preview-progress-bar {
max-width: 320px;
margin: 12px auto;
height: 6px;
background: var(--bg-tertiary);
border-radius: 3px;
overflow: hidden;
}
.xx-preview-progress-bar-wrap {
width: 100%;
height: 4px;
@@ -2680,7 +2667,7 @@
.xx-preview-video-wrapper .xx-preview-video {
max-width: 300px;
width: 100%;
aspect-ratio: 9 / 16;
aspect-ratio: 16 / 9;
margin-bottom: 0;
}
@@ -61,13 +61,23 @@ export const buildEditPlanPayload = (props: UseGenerateVideoProps) => {
* 返回错误信息,通过则返回 null
*/
export const validateGenerateInputs = (props: UseGenerateVideoProps): string | null => {
const { titleSettings, materialMode, selectedMaterials, voiceMode, selectedClonedVoice } = props
const {
titleSettings,
materialMode,
selectedMaterials,
smartSelectedIds,
voiceMode,
selectedClonedVoice,
} = props
if (!titleSettings.title.trim()) {
// AI 自动选择模式下,标题可以为空(后端会自行生成)
if (!titleSettings.aiAutoSelect && !titleSettings.title?.trim()) {
return "请先选择或输入标题"
}
if (materialMode === "manual" && selectedMaterials.length === 0) {
return "请至少选择一个素材"
// 无论手动还是自动模式,都必须有素材
const materialIds = materialMode === "auto" ? smartSelectedIds || [] : selectedMaterials || []
if (materialIds.length === 0) {
return materialMode === "auto" ? "AI 未匹配到素材,请手动选择素材后重试" : "请至少选择一个素材"
}
if (voiceMode === "clone" && !selectedClonedVoice) {
return "请先选择一个克隆音色"
@@ -1,75 +1,88 @@
import { useState, useCallback, useMemo } from "react"
import { message } from "antd"
import type { AssetItem } from "@/api/assets"
import { SMART_MATCH_REASONS } from "../../constants"
interface SmartMatchedResult {
asset: AssetItem
matchScore: number
matchReason: string
}
import { smartMatchAssets } from "@/api/assets"
interface UseSmartMatchOptions {
libraryId: string
materials: { items: AssetItem[]; total: number }
smartSelectedIds: string[]
onSmartSelectedIdsChange: (ids: string[]) => void
}
/**
* 智能素材匹配 Hook
* 封装 AI 匹配、换一批、全选/清空等逻辑
* 智能素材匹配 HookQ5 简化版)
* 用户不手动选素材时,一键调用后端 AI 选素材
* 后端统一选素材逻辑后续完善,当前先走前端流程简化
*/
export function useSmartMatch({
libraryId,
materials,
smartSelectedIds,
onSmartSelectedIdsChange,
}: UseSmartMatchOptions) {
const [smartMatchInput, setSmartMatchInput] = useState("")
const [smartMatching, setSmartMatching] = useState(false)
const [smartMatchedResults, setSmartMatchedResults] = useState<SmartMatchedResult[]>([])
const [hasMatched, setHasMatched] = useState(false)
const [smartMatchedResults, setSmartMatchedResults] = useState<AssetItem[]>([])
/* ── 智能素材匹配 ── */
/* ── 一键智能匹配 ── */
const handleSmartMatch = useCallback(async () => {
if (!smartMatchInput.trim()) {
message.warning("请先输入视频内容描述")
if (!libraryId) {
message.warning("请先选择视频库")
return
}
if (materials.items.length === 0) {
message.warning("当前视频库暂无素材")
return
}
setSmartMatching(true)
setHasMatched(true)
// 模拟 AI 匹配延迟
await new Promise((resolve) => setTimeout(resolve, 1500))
try {
// 调用后端智能匹配 API
const result = await smartMatchAssets(libraryId)
const matchedIds = result.items?.map((a: AssetItem) => a.id) ?? []
// 从素材库中随机选取 5-8 个作为推荐结果
const shuffled = [...materials.items].sort(() => Math.random() - 0.5)
const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 4))
const picked = shuffled.slice(0, count)
if (matchedIds.length > 0) {
onSmartSelectedIdsChange(matchedIds)
// 保存 API 返回的完整素材列表
const resolved = result.items?.length
? result.items
: materials.items.filter((a) => matchedIds.includes(a.id))
setSmartMatchedResults(resolved)
setHasMatched(true)
message.success(`AI 已为你选择 ${matchedIds.length} 个素材`)
} else {
// 后端返回空结果,回退到全选
onSmartSelectedIdsChange(materials.items.map((a) => a.id))
setSmartMatchedResults(materials.items)
setHasMatched(true)
message.info("AI 暂未找到匹配素材,已全选当前库素材")
}
} catch {
// 后端 API 尚未就绪时,回退到全选当前库素材
onSmartSelectedIdsChange(materials.items.map((a) => a.id))
setSmartMatchedResults(materials.items)
setHasMatched(true)
message.info("已为你全选当前库素材(智能匹配功能即将上线)")
} finally {
setSmartMatching(false)
}
}, [libraryId, materials.items, onSmartSelectedIdsChange])
const results = picked.map((asset, idx) => ({
asset,
matchScore: Math.round(85 + Math.random() * 14), // 85-99 分
matchReason:
SMART_MATCH_REASONS[idx % SMART_MATCH_REASONS.length] +
(Math.random() > 0.5 ? ",画面质感优秀" : ""),
}))
const handleRefreshMatch = useCallback(async () => {
// 换一批 = 重新触发智能匹配
await handleSmartMatch()
}, [handleSmartMatch])
// 按匹配度从高到低排序
results.sort((a, b) => b.matchScore - a.matchScore)
const handleSelectAllMatched = useCallback(() => {
onSmartSelectedIdsChange(materials.items.map((a) => a.id))
}, [materials.items, onSmartSelectedIdsChange])
setSmartMatchedResults(results)
// 默认选中匹配度 >= 90 的素材
const defaultSelected = results.filter((r) => r.matchScore >= 90).map((r) => r.asset.id)
onSmartSelectedIdsChange(
defaultSelected.length > 0 ? defaultSelected : results.slice(0, 3).map((r) => r.asset.id),
)
setSmartMatching(false)
}, [smartMatchInput, materials.items, onSmartSelectedIdsChange])
const handleClearSmartSelect = useCallback(() => {
onSmartSelectedIdsChange([])
}, [onSmartSelectedIdsChange])
const handleToggleSmartSelect = useCallback(
(assetId: string) => {
@@ -82,57 +95,20 @@ export function useSmartMatch({
[smartSelectedIds, onSmartSelectedIdsChange],
)
const handleRefreshMatch = useCallback(async () => {
if (materials.items.length <= 5) {
message.info("视频库素材较少,无法换一批")
return
}
setSmartMatching(true)
await new Promise((resolve) => setTimeout(resolve, 800))
const remaining = materials.items.filter(
(m) => !smartMatchedResults.some((r) => r.asset.id === m.id),
)
const shuffled = [...remaining].sort(() => Math.random() - 0.5)
const count = Math.min(shuffled.length, 5 + Math.floor(Math.random() * 3))
const picked = shuffled.slice(0, count)
const results = picked.map((asset, idx) => ({
asset,
matchScore: Math.round(80 + Math.random() * 19),
matchReason:
SMART_MATCH_REASONS[(idx + 2) % SMART_MATCH_REASONS.length] +
(Math.random() > 0.5 ? ",节奏明快" : ""),
}))
results.sort((a, b) => b.matchScore - a.matchScore)
setSmartMatchedResults(results)
onSmartSelectedIdsChange([])
setSmartMatching(false)
}, [materials.items, smartMatchedResults, onSmartSelectedIdsChange])
const handleSelectAllMatched = useCallback(() => {
onSmartSelectedIdsChange(smartMatchedResults.map((r) => r.asset.id))
}, [smartMatchedResults, onSmartSelectedIdsChange])
const handleClearSmartSelect = useCallback(() => {
onSmartSelectedIdsChange([])
}, [onSmartSelectedIdsChange])
/* ── 计算已选智能匹配素材的总时长 ── */
const smartSelectedTotalDuration = useMemo(() => {
return smartMatchedResults
.filter((r) => smartSelectedIds.includes(r.asset.id))
.reduce((sum, r) => sum + (r.asset.duration || 0), 0)
}, [smartMatchedResults, smartSelectedIds])
/* ── 计算已选素材总时长 ── */
const smartSelectedTotalDuration = useMemo(
() =>
materials.items
.filter((a) => smartSelectedIds.includes(a.id))
.reduce((sum, a) => sum + (a.duration || 0), 0),
[materials.items, smartSelectedIds],
)
return {
smartMatchInput,
setSmartMatchInput,
smartMatching,
smartMatchedResults,
hasMatched,
smartSelectedIds,
smartMatchedResults,
handleSmartMatch,
handleToggleSmartSelect,
handleRefreshMatch,
@@ -2,7 +2,7 @@
* Step 2 素材选择 Hook
* 组合素材库加载 + 智能匹配两个子 Hook
*/
import { useCallback } from "react"
import { useCallback, useEffect, useRef } from "react"
import { formatDuration } from "../utils/formatDuration"
import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary"
import { useSmartMatch } from "./step2-materials/useSmartMatch"
@@ -28,11 +28,31 @@ export function useStep2Materials({
useMaterialLibrary()
const smartMatch = useSmartMatch({
libraryId: selectedLibraryId,
materials,
smartSelectedIds,
onSmartSelectedIdsChange,
})
/* ── 自动触发智能匹配:选择视频库后自动调用 ── */
const autoTriggeredRef = useRef<string>("")
const { handleSmartMatch } = smartMatch
useEffect(() => {
// 离开 auto 模式时重置,确保下次进入 auto 模式能重新触发
if (materialMode !== "auto") {
autoTriggeredRef.current = ""
return
}
if (!selectedLibraryId) return
if (materialsLoading) return
if (materials.items.length === 0) return
// 防止同一视频库重复触发
if (autoTriggeredRef.current === selectedLibraryId) return
autoTriggeredRef.current = selectedLibraryId
handleSmartMatch()
}, [selectedLibraryId, materialMode, materialsLoading, materials.items, handleSmartMatch])
/* ── 手动选择素材 ── */
const handleToggleMaterial = useCallback(
(materialId: string) => {
@@ -58,13 +78,11 @@ export function useStep2Materials({
// 手动选择
selectedMaterials,
handleToggleMaterial,
// 智能匹配
smartMatchInput: smartMatch.smartMatchInput,
setSmartMatchInput: smartMatch.setSmartMatchInput,
// 智能匹配(简化版)
smartMatching: smartMatch.smartMatching,
smartMatchedResults: smartMatch.smartMatchedResults,
hasMatched: smartMatch.hasMatched,
smartSelectedIds: smartMatch.smartSelectedIds,
smartMatchedResults: smartMatch.smartMatchedResults,
handleSmartMatch: smartMatch.handleSmartMatch,
handleToggleSmartSelect: smartMatch.handleToggleSmartSelect,
handleRefreshMatch: smartMatch.handleRefreshMatch,
@@ -1,308 +0,0 @@
/**
* Step 3 生成预览 Hook
* 调用 /generation/preview 接口创建预览任务,轮询状态直到完成
*/
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation"
import type { PreviewTaskResponse, PreviewStatus as ApiPreviewStatus } from "@/api/generation"
import type { EditingTemplate } from "@/api/editing-planner"
import { safeExtractError } from "./generate-video/errorUtils"
/** 安全地将值转为字符串,防止对象被直接渲染导致 React Error #31 */
const safeString = (val: unknown, fallback: string): string => {
if (val == null) return fallback
const s = safeExtractError(val)
return s || fallback
}
/** 安全地将值转为数字,防止非数字值进入渲染 */
const safeNumber = (val: unknown, fallback = 0): number => {
if (typeof val === "number" && !Number.isNaN(val)) return val
if (typeof val === "string") {
const n = Number(val)
return Number.isNaN(n) ? fallback : n
}
return fallback
}
interface UseStep3PreviewProps {
templates: EditingTemplate[]
selectedTemplate: string
materialMode: "manual" | "auto"
selectedMaterials: string[]
smartSelectedIds: string[]
duration: number
videoRatio: string
titleText?: string
voiceId?: string
}
export type PreviewStatus = "idle" | "pending" | "generating" | "ready" | "error"
/** 预览生成结果 */
export interface PreviewResult {
taskId: string
videoUrl: string
clipCount: number
transitionCount: number
materialUsage: number
duration: number
fileSize: number
generateDuration: number
progress: number
}
// 轮询超时时间(10 分钟)
const POLL_TIMEOUT_MS = 10 * 60 * 1000
export function useStep3Preview({
templates,
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
duration,
videoRatio,
}: UseStep3PreviewProps) {
const templateName = useMemo(
() => templates.find((t) => t.id === selectedTemplate)?.name ?? "未选择",
[templates, selectedTemplate],
)
const materialCount = useMemo(() => {
if (materialMode === "auto") {
return `${smartSelectedIds.length} 个素材(智能匹配)`
}
return `${selectedMaterials.length} 个素材`
}, [materialMode, selectedMaterials.length, smartSelectedIds.length])
const materialTotal = materialMode === "auto" ? smartSelectedIds.length : selectedMaterials.length
/* ── 预览生成状态 ── */
const [previewStatus, setPreviewStatus] = useState<PreviewStatus>("idle")
const [previewResult, setPreviewResult] = useState<PreviewResult | null>(null)
const [previewError, setPreviewError] = useState<string>("")
const [progress, setProgress] = useState(0)
// 任务 ID + 轮询定时器,用于防止竞态条件
const currentTaskIdRef = useRef<string | null>(null)
const pollTimerRef = useRef<ReturnType<typeof setTimeout>>()
const startTimeRef = useRef<number>(0)
const clearPollTimer = useCallback(() => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current)
pollTimerRef.current = undefined
}
}, [])
/* ── 参数变化时重置预览状态 ── */
const prevDepsRef = useRef({
selectedTemplate,
materialMode,
selectedMaterials: [...selectedMaterials].sort().join(","),
smartSelectedIds: [...smartSelectedIds].sort().join(","),
duration,
videoRatio,
})
useEffect(() => {
const currentKey = [
selectedTemplate,
materialMode,
[...selectedMaterials].sort().join(","),
[...smartSelectedIds].sort().join(","),
duration,
videoRatio,
].join("|")
const prevKey = [
prevDepsRef.current.selectedTemplate,
prevDepsRef.current.materialMode,
prevDepsRef.current.selectedMaterials,
prevDepsRef.current.smartSelectedIds,
prevDepsRef.current.duration,
prevDepsRef.current.videoRatio,
].join("|")
if (prevKey !== currentKey && previewStatus !== "idle") {
// 参数变化,作废当前任务
currentTaskIdRef.current = null
clearPollTimer()
setPreviewStatus("idle")
setPreviewResult(null)
setPreviewError("")
setProgress(0)
}
prevDepsRef.current = {
selectedTemplate,
materialMode,
selectedMaterials: [...selectedMaterials].sort().join(","),
smartSelectedIds: [...smartSelectedIds].sort().join(","),
duration,
videoRatio,
}
}, [
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
duration,
videoRatio,
previewStatus,
clearPollTimer,
])
// 组件卸载时清理轮询
useEffect(() => {
return () => {
clearPollTimer()
}
}, [clearPollTimer])
/** 轮询预览任务状态 */
const pollPreviewStatus = useCallback((taskId: string) => {
const poll = async () => {
// 检查是否已被取消(参数变化或重新生成)
if (currentTaskIdRef.current !== taskId) return
// 超时检查
if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) {
setPreviewError("预览生成超时,请重试")
setPreviewStatus("error")
return
}
try {
const data: PreviewTaskResponse = await getPreviewStatus(taskId)
if (currentTaskIdRef.current !== taskId) return
const status = data.status as ApiPreviewStatus
if (status === "completed") {
setProgress(100)
setPreviewResult({
taskId: safeString(data.task_id, ""),
videoUrl: safeString(data.video_url, ""),
clipCount: safeNumber(data.clip_count),
transitionCount: safeNumber(data.transition_count),
materialUsage: safeNumber(data.material_usage),
duration: safeNumber(data.duration),
fileSize: safeNumber(data.file_size),
generateDuration: safeNumber(data.generate_duration),
progress: 100,
})
setPreviewStatus("ready")
return
}
if (status === "failed") {
setPreviewError(safeString(data.error_message, "预览生成失败,请重试"))
setPreviewStatus("error")
return
}
if (status === "cancelled") {
setPreviewError("预览任务已取消")
setPreviewStatus("error")
return
}
// pending / generating 状态继续轮询
setProgress(safeNumber(data.progress))
if (status === "pending") {
setPreviewStatus("pending")
pollTimerRef.current = setTimeout(poll, 5000)
} else {
setPreviewStatus("generating")
pollTimerRef.current = setTimeout(poll, 2000)
}
} catch (pollErr) {
if (currentTaskIdRef.current !== taskId) return
// 轮询出错,延迟后重试
pollTimerRef.current = setTimeout(poll, 3000)
}
}
// 首次延迟 1 秒开始轮询
pollTimerRef.current = setTimeout(poll, 1000)
}, [])
/** 生成预览 */
const generatePreview = useCallback(async () => {
if (!selectedTemplate) {
setPreviewError("请先选择模板")
setPreviewStatus("error")
return
}
if (materialTotal === 0) {
setPreviewError("请先选择素材")
setPreviewStatus("error")
return
}
// 取消之前的轮询
clearPollTimer()
setPreviewStatus("pending")
setPreviewError("")
setProgress(0)
setPreviewResult(null)
startTimeRef.current = Date.now()
try {
const assetIds = materialMode === "auto" ? smartSelectedIds : selectedMaterials
const response = await createPreview({
template_id: selectedTemplate,
asset_ids: assetIds,
duration: duration || undefined,
video_ratio: videoRatio,
})
// 竞态检查
if (startTimeRef.current === 0) return // 已被重置
currentTaskIdRef.current = response.task_id
pollPreviewStatus(response.task_id)
} catch (e) {
setPreviewError(safeString(e instanceof Error ? e.message : e, "预览生成失败"))
setPreviewStatus("error")
}
}, [
selectedTemplate,
materialTotal,
materialMode,
smartSelectedIds,
selectedMaterials,
duration,
videoRatio,
clearPollTimer,
pollPreviewStatus,
])
/** 重新生成预览 */
const regeneratePreview = useCallback(() => {
generatePreview()
}, [generatePreview])
/** 是否可以下一步(预览已生成) */
const canProceed = previewStatus === "ready"
return {
templateName,
materialCount,
duration,
videoRatio,
// 预览生成状态
previewStatus,
previewResult,
previewError,
progress,
canProceed,
generatePreview,
regeneratePreview,
}
}
export default useStep3Preview
@@ -0,0 +1,430 @@
/**
* Step 4 生成预览 Hook(支持多预览 + voice_ids
* 调用 /generation/preview 接口创建多个预览任务,轮询状态直到全部完成
*/
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation"
import type { PreviewTaskResponse, PreviewStatus as ApiPreviewStatus } from "@/api/generation"
import type { EditingTemplate } from "@/api/editing-planner"
import { safeExtractError } from "./generate-video/errorUtils"
/** 安全地将值转为字符串,防止对象被直接渲染导致 React Error #31 */
const safeString = (val: unknown, fallback: string): string => {
if (val == null) return fallback
const s = safeExtractError(val)
return s || fallback
}
/** 安全地将值转为数字,防止非数字值进入渲染 */
const safeNumber = (val: unknown, fallback = 0): number => {
if (typeof val === "number" && !Number.isNaN(val)) return val
if (typeof val === "string") {
const n = Number(val)
return Number.isNaN(n) ? fallback : n
}
return fallback
}
interface UseStep4PreviewProps {
templates: EditingTemplate[]
selectedTemplate: string
materialMode: "manual" | "auto"
selectedMaterials: string[]
smartSelectedIds: string[]
duration: number
videoRatio: string
/** 配音 voice_ids(传给后端,让预览包含配音音频) */
voiceIds?: string[]
/** 要生成的预览数量 */
previewCount?: number
}
export type PreviewStatus = "idle" | "pending" | "generating" | "ready" | "error"
/** 单个预览生成结果 */
export interface PreviewResult {
taskId: string
videoUrl: string
clipCount: number
transitionCount: number
materialUsage: number
duration: number
fileSize: number
generateDuration: number
progress: number
}
/** 单个预览项的完整状态(用于多预览) */
export interface PreviewItem {
index: number
status: PreviewStatus
result: PreviewResult | null
error: string
progress: number
}
// 轮询超时时间(10 分钟)
const POLL_TIMEOUT_MS = 10 * 60 * 1000
/** 初始单项状态 */
const createInitialItem = (index: number): PreviewItem => ({
index,
status: "idle",
result: null,
error: "",
progress: 0,
})
export function useStep4Preview({
templates,
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
duration,
videoRatio,
voiceIds,
previewCount = 1,
}: UseStep4PreviewProps) {
const templateName = useMemo(
() => templates.find((t) => t.id === selectedTemplate)?.name ?? "未选择",
[templates, selectedTemplate],
)
const materialCount = useMemo(() => {
if (materialMode === "auto") {
return `${smartSelectedIds.length} 个素材(智能匹配)`
}
return `${selectedMaterials.length} 个素材`
}, [materialMode, selectedMaterials.length, smartSelectedIds.length])
const materialTotal = materialMode === "auto" ? smartSelectedIds.length : selectedMaterials.length
/* ── 多预览状态 ── */
const [items, setItems] = useState<PreviewItem[]>(() =>
Array.from({ length: previewCount }, (_, i) => createInitialItem(i)),
)
const [selectedIndex, setSelectedIndex] = useState(0)
// 每个任务 ID + 轮询定时器,用于防止竞态条件(按 index 存储)
const taskIdsRef = useRef<Map<number, string>>(new Map())
const pollTimersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(new Map())
const startTimeRef = useRef<number>(0)
const clearPollTimer = useCallback((index?: number) => {
if (index !== undefined) {
const timer = pollTimersRef.current.get(index)
if (timer) {
clearTimeout(timer)
pollTimersRef.current.delete(index)
}
} else {
pollTimersRef.current.forEach((timer) => clearTimeout(timer))
pollTimersRef.current.clear()
}
}, [])
// 同步 previewCount 变化(增减项)
useEffect(() => {
setItems((prev) => {
if (prev.length === previewCount) return prev
if (prev.length > previewCount) return prev.slice(0, previewCount)
return [
...prev,
...Array.from({ length: previewCount - prev.length }, (_, i) =>
createInitialItem(prev.length + i),
),
]
})
// 如果 selectedIndex 超出范围,重置
setSelectedIndex((prev) => Math.min(prev, previewCount - 1))
}, [previewCount])
/* ── 参数变化时重置所有预览状态 ── */
const prevDepsRef = useRef({
selectedTemplate,
materialMode,
selectedMaterials: [...selectedMaterials].sort().join(","),
smartSelectedIds: [...smartSelectedIds].sort().join(","),
duration,
videoRatio,
voiceIds: [...(voiceIds || [])].sort().join(","),
})
useEffect(() => {
const currentKey = [
selectedTemplate,
materialMode,
[...selectedMaterials].sort().join(","),
[...smartSelectedIds].sort().join(","),
duration,
videoRatio,
[...(voiceIds || [])].sort().join(","),
].join("|")
const prevKey = [
prevDepsRef.current.selectedTemplate,
prevDepsRef.current.materialMode,
prevDepsRef.current.selectedMaterials,
prevDepsRef.current.smartSelectedIds,
prevDepsRef.current.duration,
prevDepsRef.current.videoRatio,
prevDepsRef.current.voiceIds,
].join("|")
if (prevKey !== currentKey && items.some((it) => it.status !== "idle")) {
taskIdsRef.current.clear()
clearPollTimer()
setItems(Array.from({ length: previewCount }, (_, i) => createInitialItem(i)))
setSelectedIndex(0)
}
prevDepsRef.current = {
selectedTemplate,
materialMode,
selectedMaterials: [...selectedMaterials].sort().join(","),
smartSelectedIds: [...smartSelectedIds].sort().join(","),
duration,
videoRatio,
voiceIds: [...(voiceIds || [])].sort().join(","),
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
duration,
videoRatio,
voiceIds,
previewCount,
])
// 组件卸载时清理所有轮询
useEffect(() => {
return () => {
clearPollTimer()
}
}, [clearPollTimer])
/** 轮询单个预览任务状态 */
const pollPreviewStatus = useCallback((index: number, taskId: string) => {
const poll = async () => {
// 竞态检查
if (taskIdsRef.current.get(index) !== taskId) return
// 超时检查
if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) {
setItems((prev) =>
prev.map((it) =>
it.index === index ? { ...it, status: "error", error: "预览生成超时,请重试" } : it,
),
)
return
}
try {
const data: PreviewTaskResponse = await getPreviewStatus(taskId)
if (taskIdsRef.current.get(index) !== taskId) return
const status = data.status as ApiPreviewStatus
if (status === "completed") {
const result: PreviewResult = {
taskId: safeString(data.task_id, ""),
videoUrl: safeString(data.video_url, ""),
clipCount: safeNumber(data.clip_count),
transitionCount: safeNumber(data.transition_count),
materialUsage: safeNumber(data.material_usage),
duration: safeNumber(data.duration),
fileSize: safeNumber(data.file_size),
generateDuration: safeNumber(data.generate_duration),
progress: 100,
}
setItems((prev) =>
prev.map((it) =>
it.index === index ? { ...it, status: "ready", result, progress: 100 } : it,
),
)
return
}
if (status === "failed") {
setItems((prev) =>
prev.map((it) =>
it.index === index
? {
...it,
status: "error",
error: safeString(data.error_message, "预览生成失败,请重试"),
}
: it,
),
)
return
}
if (status === "cancelled") {
setItems((prev) =>
prev.map((it) =>
it.index === index ? { ...it, status: "error", error: "预览任务已取消" } : it,
),
)
return
}
// pending / generating 状态继续轮询
const prog = safeNumber(data.progress)
const nextStatus: PreviewStatus = status === "pending" ? "pending" : "generating"
setItems((prev) =>
prev.map((it) =>
it.index === index ? { ...it, status: nextStatus, progress: prog } : it,
),
)
const delay = status === "pending" ? 5000 : 2000
pollTimersRef.current.set(index, setTimeout(poll, delay))
} catch {
if (taskIdsRef.current.get(index) !== taskId) return
pollTimersRef.current.set(index, setTimeout(poll, 3000))
}
}
pollTimersRef.current.set(index, setTimeout(poll, 1000))
}, [])
/** 生成所有预览 */
const generatePreview = useCallback(async () => {
if (!selectedTemplate) {
setItems((prev) => prev.map((it) => ({ ...it, status: "error", error: "请先选择模板" })))
return
}
if (materialTotal === 0) {
setItems((prev) => prev.map((it) => ({ ...it, status: "error", error: "请先选择素材" })))
return
}
// 取消之前的所有轮询
clearPollTimer()
taskIdsRef.current.clear()
// 初始化所有项为 pending
setItems(
Array.from({ length: previewCount }, (_, i) => ({
index: i,
status: "pending" as PreviewStatus,
result: null,
error: "",
progress: 0,
})),
)
setSelectedIndex(0)
startTimeRef.current = Date.now()
const assetIds = materialMode === "auto" ? smartSelectedIds : selectedMaterials
// 并发创建所有预览任务(Promise.all 并行请求,减少串行等待)
const createTasks = Array.from({ length: previewCount }, async (_, i) => {
try {
const response = await createPreview({
template_id: selectedTemplate,
asset_ids: assetIds,
duration: duration || undefined,
video_ratio: videoRatio,
voice_ids: voiceIds && voiceIds.length > 0 ? voiceIds : undefined,
})
if (startTimeRef.current === 0) return
taskIdsRef.current.set(i, response.task_id)
pollPreviewStatus(i, response.task_id)
} catch (e) {
const errMsg = safeString(e instanceof Error ? e.message : e, "预览生成失败")
setItems((prev) =>
prev.map((it) => (it.index === i ? { ...it, status: "error", error: errMsg } : it)),
)
}
})
await Promise.all(createTasks)
}, [
selectedTemplate,
materialTotal,
materialMode,
smartSelectedIds,
selectedMaterials,
duration,
videoRatio,
voiceIds,
previewCount,
clearPollTimer,
pollPreviewStatus,
])
/** 重新生成所有预览 */
const regeneratePreview = useCallback(() => {
generatePreview()
}, [generatePreview])
/** 是否所有预览都已完成 */
const allReady = items.length > 0 && items.every((it) => it.status === "ready")
/** 是否至少有一个预览已完成 */
const anyReady = items.some((it) => it.status === "ready")
/** 是否有任一正在生成中 */
const anyGenerating = items.some((it) => it.status === "pending" || it.status === "generating")
/** 当前选中的预览结果 */
const selectedResult = items[selectedIndex]?.result ?? null
/** 综合状态(兼容旧逻辑) */
const previewStatus: PreviewStatus = useMemo(() => {
if (items.every((it) => it.status === "idle")) return "idle"
if (items.some((it) => it.status === "pending" || it.status === "generating"))
return "generating"
if (allReady) return "ready"
if (items.every((it) => it.status === "error")) return "error"
// 部分完成部分出错
if (anyReady) return "ready"
return "error"
}, [items, allReady, anyReady])
/** 综合进度(取平均) */
const progress = useMemo(() => {
if (items.length === 0) return 0
return Math.round(items.reduce((sum, it) => sum + it.progress, 0) / items.length)
}, [items])
/** 综合错误信息 */
const previewError = useMemo(() => {
const errorItems = items.filter((it) => it.status === "error" && it.error)
if (errorItems.length === 0) return ""
if (errorItems.length === 1) return errorItems[0].error
return `${errorItems.length} 个预览生成失败`
}, [items])
const canProceed = anyReady
return {
templateName,
materialCount,
duration,
videoRatio,
// 多预览状态
items,
selectedIndex,
setSelectedIndex,
previewCount,
// 综合状态
previewStatus,
previewResult: selectedResult,
previewError,
progress,
canProceed,
allReady,
anyReady,
anyGenerating,
generatePreview,
regeneratePreview,
}
}
export default useStep4Preview
@@ -1,3 +1,4 @@
import { useEffect, useRef } from "react"
import { useQuery } from "@tanstack/react-query"
import { getTitles } from "@/api/titles"
import type { TitleSettings } from "../../types"
@@ -22,27 +23,59 @@ export function useStep4Title({ titleSettings, onTitleSettingsChange }: UseStep4
})
// AI 标题生成
const aiGenerator = useAiTitleGenerator({ titleSettings, onTitleSettingsChange })
const {
aiTitleInput,
setAiTitleInput,
aiTitleGenerating,
aiTitleResults,
hasGeneratedTitles,
handleGenerateAiTitles,
handleSelectAiTitle,
handleRefreshAiTitles,
autoGenerateTitle,
} = useAiTitleGenerator({ titleSettings, onTitleSettingsChange })
// 样式更新
const styleUpdaters = useTitleStyleUpdaters({ titleSettings, onTitleSettingsChange })
// 追踪 AI 自动选择开关的上一次值 & 是否首次挂载
const prevAiAutoSelect = useRef(titleSettings.aiAutoSelect)
const isFirstMount = useRef(true)
// 当 AI 自动选择开关打开时,自动生成/选择一个标题填入
// 首次挂载时如果开关已经是 true 且无标题,也需要触发
useEffect(() => {
if (isFirstMount.current) {
isFirstMount.current = false
if (titleSettings.aiAutoSelect && !titleSettings.title) {
autoGenerateTitle()
}
prevAiAutoSelect.current = titleSettings.aiAutoSelect
return
}
if (titleSettings.aiAutoSelect && !prevAiAutoSelect.current && !titleSettings.title) {
autoGenerateTitle()
}
prevAiAutoSelect.current = titleSettings.aiAutoSelect
}, [titleSettings.aiAutoSelect, titleSettings.title, autoGenerateTitle])
return {
// 数据
userTitles,
titleSettings,
// AI 标题状态
aiTitleInput: aiGenerator.aiTitleInput,
setAiTitleInput: aiGenerator.setAiTitleInput,
aiTitleGenerating: aiGenerator.aiTitleGenerating,
aiTitleResults: aiGenerator.aiTitleResults,
hasGeneratedTitles: aiGenerator.hasGeneratedTitles,
aiTitleInput,
setAiTitleInput,
aiTitleGenerating,
aiTitleResults,
hasGeneratedTitles,
activePreset: styleUpdaters.activePreset,
titlePresets: styleUpdaters.titlePresets,
// AI 标题操作
handleGenerateAiTitles: aiGenerator.handleGenerateAiTitles,
handleSelectAiTitle: aiGenerator.handleSelectAiTitle,
handleRefreshAiTitles: aiGenerator.handleRefreshAiTitles,
handleGenerateAiTitles,
handleSelectAiTitle,
handleRefreshAiTitles,
autoGenerateTitle,
// 标题设置操作
updateTitle: styleUpdaters.updateTitle,
toggleAiAutoSelect: styleUpdaters.toggleAiAutoSelect,
@@ -72,9 +72,30 @@ export function useAiTitleGenerator({
setAiTitleGenerating(false)
}, [aiTitleInput])
/**
* 自动生成标题(供 AI 自动选择开关使用)
* 如果已有生成结果,直接从中选一个;否则用默认关键词生成
*/
const autoGenerateTitle = useCallback((): string => {
if (aiTitleResults.length > 0) {
const picked = aiTitleResults[Math.floor(Math.random() * aiTitleResults.length)]
if (!picked) return ""
onTitleSettingsChange({ ...titleSettings, title: picked.title })
return picked.title
}
// 没有已有结果,用默认关键词生成
const results = generateTitlesFromTopic("短视频")
setAiTitleResults(results)
setHasGeneratedTitles(true)
const picked = results[Math.floor(Math.random() * results.length)]
if (!picked) return ""
onTitleSettingsChange({ ...titleSettings, title: picked.title })
return picked.title
}, [aiTitleResults, titleSettings, onTitleSettingsChange])
const handleSelectAiTitle = useCallback(
(title: string) => {
onTitleSettingsChange({ ...titleSettings, title, aiAutoSelect: false })
onTitleSettingsChange({ ...titleSettings, title })
message.success("已选用此标题")
},
[titleSettings, onTitleSettingsChange],
@@ -98,5 +119,6 @@ export function useAiTitleGenerator({
handleGenerateAiTitles,
handleSelectAiTitle,
handleRefreshAiTitles,
autoGenerateTitle,
}
}
@@ -15,10 +15,10 @@ export function useTitleStyleUpdaters({
titleSettings,
onTitleSettingsChange,
}: UseTitleStyleUpdatersOptions) {
/** 匹配预设:只用 color/bold/italic/stroke/shadow,不再匹配 size */
const getActivePreset = (settings: TitleSettings): string | null => {
for (const p of TITLE_PRESETS) {
if (
settings.size === p.style.size &&
settings.color === p.style.color &&
settings.bold === p.style.bold &&
settings.italic === p.style.italic &&
@@ -88,13 +88,13 @@ export function useTitleStyleUpdaters({
onTitleSettingsChange({ ...titleSettings, shadow: !titleSettings.shadow })
}, [titleSettings, onTitleSettingsChange])
/** 应用预设:只覆盖 color/bold/italic/stroke/shadow,不改变字号 */
const applyPreset = useCallback(
(presetKey: string) => {
const preset = TITLE_PRESETS.find((p) => p.key === presetKey)
if (!preset) return
onTitleSettingsChange({
...titleSettings,
size: preset.style.size,
color: preset.style.color,
bold: preset.style.bold,
italic: preset.style.italic,
@@ -2,21 +2,30 @@
* Step 6 封面设置 Hook
* 封装封面设置的交互逻辑
*/
import { useCallback } from "react"
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 { generateCover } from "@/api/template-editor"
interface UseStep6CoverProps {
coverSettings: CoverConfig
onCoverSettingsChange: (settings: CoverConfig) => void
duration: number
/** 当前素材 ID 列表,用于智能封面生成 */
assetIds?: string[]
/** 当前选中的模板 ID */
selectedTemplate?: string
}
export function useStep6Cover({
coverSettings,
onCoverSettingsChange,
duration,
assetIds = [],
selectedTemplate = "",
}: UseStep6CoverProps) {
const generatingRef = useRef(false)
const formatTime = useCallback((seconds: number) => {
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
@@ -62,6 +71,30 @@ export function useStep6Cover({
[coverSettings, onCoverSettingsChange],
)
/** 调用后端智能封面 API,生成封面并更新预览 */
const generateAutoCover = useCallback(async () => {
if (!selectedTemplate || assetIds.length === 0 || generatingRef.current) return
generatingRef.current = true
try {
const response = await generateCover(selectedTemplate, {
asset_ids: assetIds,
cover_type: "ai_frame",
})
const thumbnailUrl = response.cover?.thumbnail_url || ""
if (thumbnailUrl) {
onCoverSettingsChange({
...coverSettings,
thumbnail_url: thumbnailUrl,
ai_suggested_time: response.cover?.frame_time ?? null,
})
}
} catch (err) {
console.warn("[Step6] 智能封面生成失败:", err)
} finally {
generatingRef.current = false
}
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange])
const totalDuration = duration || 30
return {
@@ -71,6 +104,7 @@ export function useStep6Cover({
setMode,
setFrameTime,
handleUpload,
generateAutoCover,
totalDuration,
COVER_MODE_LABELS,
COVER_MODE_ICONS,
@@ -3,11 +3,13 @@
* 封装生成确认页的展示逻辑
*/
import { useMemo } from "react"
import { useQuery } from "@tanstack/react-query"
import type { EditingTemplate } from "@/api/editing-planner"
import type { GeneratedVideo } from "@/api/template-editor"
import type { CoverConfig } from "../../editing-planner/types"
import type { VoiceClone } from "@/api/voice-clone"
import type { PresetVoiceItem } from "@/api/voices"
import { getAssetsByKind } from "@/api/assets"
import { COVER_MODE_LABELS } from "../constants"
interface UseStep7GenerateProps {
@@ -39,11 +41,11 @@ export function useStep7Generate({
selectedMaterials,
smartSelectedIds,
title,
voiceMode,
voiceMode: _voiceMode,
selectedVoice,
selectedClonedVoice,
presetVoices,
clonedVoices,
selectedClonedVoice: _selectedClonedVoice,
presetVoices: _presetVoices,
clonedVoices: _clonedVoices,
coverSettings,
generateCount,
onGenerateCountChange,
@@ -65,14 +67,16 @@ export function useStep7Generate({
return `${selectedMaterials.length} 个素材`
}, [materialMode, selectedMaterials.length, smartSelectedIds.length])
// 从配音素材库中查找 voiceName
const { data: voiceMaterials = [] } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
const voiceName = useMemo(() => {
if (voiceMode === "clone") {
const cv = clonedVoices.find((v) => v.id === selectedClonedVoice)
return cv ? cv.name : "未选择"
}
const pv = presetVoices.find((v) => v.voice_id === selectedVoice)
return pv ? pv.name : "未选择"
}, [voiceMode, selectedVoice, selectedClonedVoice, presetVoices, clonedVoices])
const asset = voiceMaterials.find((v) => v.id === selectedVoice)
return asset ? asset.name : "未选择"
}, [voiceMaterials, selectedVoice])
const coverSummary = useMemo(() => {
if (!coverSettings.enabled) return "不使用"
@@ -1,6 +1,7 @@
/**
* GeneratePage 步骤导航
* 管理步骤切换与各步骤的前置校验
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 预览(4) → 标题(5) → 封面(6) → 确认(7)
*/
import { message } from "antd"
import type { TitleSettings } from "../types"
@@ -13,7 +14,7 @@ export interface UseStepNavigationOptions {
selectedMaterials: string[]
smartSelectedIds: string[]
titleSettings: TitleSettings
/** Step3 是否已生成预览Step3校验用) */
/** Step4 是否已生成预览 */
previewReady: boolean
}
@@ -47,11 +48,12 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav
message.warning("请先进行智能匹配并选择素材")
return
}
if (currentStep === 3 && !previewReady) {
// Step3 配音:配音为可选项,不强制校验,用户可跳过
if (currentStep === 4 && !previewReady) {
message.warning("请先生成剪辑预览")
return
}
if (currentStep === 4 && !titleSettings.title.trim()) {
if (currentStep === 5 && !titleSettings.title.trim()) {
message.warning("请选择或输入标题")
return
}
@@ -117,11 +117,9 @@ const VoiceLibrary: React.FC = () => {
uploadOpen,
uploadFile,
uploadName,
uploadGender,
uploadDesc,
uploadProgress,
setUploadName,
setUploadGender,
setUploadDesc,
setUploadOpen,
handleFileSelect,
@@ -252,14 +250,12 @@ const VoiceLibrary: React.FC = () => {
uploadOpen={uploadOpen}
uploadFile={uploadFile}
uploadName={uploadName}
uploadGender={uploadGender}
uploadDesc={uploadDesc}
uploadProgress={uploadProgress}
onUploadClose={handleUploadClose}
onFileSelect={handleFileSelect}
onFileRemove={handleFileRemove}
onNameChange={setUploadName}
onGenderChange={setUploadGender}
onDescChange={setUploadDesc}
onUpload={handleUpload}
ttsOpen={ttsOpen}
@@ -14,14 +14,12 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
open,
uploadFile,
uploadName,
uploadGender,
uploadDesc,
uploadProgress,
onClose,
onFileSelect,
onFileRemove,
onNameChange,
onGenderChange,
onDescChange,
onUpload,
}) => {
@@ -30,7 +28,7 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
return (
<Modal
title="上传音频"
title={<span style={{ fontSize: 16, fontWeight: 600 }}></span>}
open={open}
onCancel={() => {
if (uploading) return // 上传中不可关闭
@@ -45,7 +43,7 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
display: "flex",
flexDirection: "column",
gap: 16,
padding: "8px 0",
padding: "12px 0 4px",
}}
>
{/* 拖拽上传区 */}
@@ -64,11 +62,9 @@ const UploadVoiceModal: React.FC<UploadVoiceModalProps> = ({
{/* 表单字段 */}
<FormFields
name={uploadName}
gender={uploadGender}
desc={uploadDesc}
disabled={uploading}
onNameChange={onNameChange}
onGenderChange={onGenderChange}
onDescChange={onDescChange}
/>
@@ -2,7 +2,7 @@
* VoiceLibrary 弹窗集合
*/
import React from "react"
import type { ClonedVoiceDisplay, PresetVoiceDisplay, VoiceGender } from "../types"
import type { ClonedVoiceDisplay, PresetVoiceDisplay } from "../types"
import type { VoiceClone } from "@/api/voice-clone"
import type { TtsStatus } from "./TtsModal"
import CloneModal from "@/components/voice/CloneModal"
@@ -27,14 +27,12 @@ export interface VoiceModalsProps {
uploadOpen: boolean
uploadFile: File | null
uploadName: string
uploadGender: VoiceGender
uploadDesc: string
uploadProgress: number | null
onUploadClose: () => void
onFileSelect: (file: File) => void
onFileRemove: () => void
onNameChange: (name: string) => void
onGenderChange: (gender: VoiceGender) => void
onDescChange: (desc: string) => void
onUpload: () => void
@@ -67,14 +65,12 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
uploadOpen,
uploadFile,
uploadName,
uploadGender,
uploadDesc,
uploadProgress,
onUploadClose,
onFileSelect,
onFileRemove,
onNameChange,
onGenderChange,
onDescChange,
onUpload,
ttsOpen,
@@ -113,14 +109,12 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
open={uploadOpen}
uploadFile={uploadFile}
uploadName={uploadName}
uploadGender={uploadGender}
uploadDesc={uploadDesc}
uploadProgress={uploadProgress}
onClose={onUploadClose}
onFileSelect={onFileSelect}
onFileRemove={onFileRemove}
onNameChange={onNameChange}
onGenderChange={onGenderChange}
onDescChange={onDescChange}
onUpload={onUpload}
/>
@@ -1,4 +1,5 @@
import React from "react"
import { Button } from "antd"
interface ActionButtonsProps {
uploading: boolean
@@ -13,12 +14,6 @@ const ActionButtons: React.FC<ActionButtonsProps> = ({
onCancel,
onUpload,
}) => {
const disabled = uploading || !canUpload
const handleUpload = () => {
onUpload()
}
return (
<div
style={{
@@ -28,39 +23,32 @@ const ActionButtons: React.FC<ActionButtonsProps> = ({
paddingTop: 4,
}}
>
<button
type="button"
<Button
onClick={onCancel}
disabled={uploading}
style={{
padding: "8px 20px",
height: 36,
borderRadius: 8,
border: "1px solid var(--border-color)",
background: "transparent",
fontSize: 13,
cursor: uploading ? "not-allowed" : "pointer",
color: "var(--text-secondary)",
}}
>
</button>
<button
type="button"
onClick={handleUpload}
disabled={disabled}
</Button>
<Button
type="primary"
onClick={onUpload}
disabled={!canUpload}
loading={uploading}
style={{
padding: "8px 20px",
height: 36,
borderRadius: 8,
border: "none",
background: disabled ? "var(--text-tertiary)" : "var(--primary-color)",
color: "#fff",
fontSize: 13,
fontWeight: 600,
cursor: disabled ? "not-allowed" : "pointer",
minWidth: 100,
}}
>
{uploading ? "上传中..." : "开始上传"}
</button>
{uploading ? "上传中" : "开始上传"}
</Button>
</div>
)
}
@@ -10,28 +10,55 @@ const FileInfoCard: React.FC<FileInfoCardProps> = ({ file }) => {
return (
<div
style={{
padding: "10px 12px",
padding: "10px 14px",
background: "var(--bg-secondary)",
borderRadius: 8,
borderRadius: 10,
display: "flex",
alignItems: "center",
gap: 10,
gap: 12,
border: "1px solid var(--border-color)",
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.04)",
}}
>
<SoundOutlined style={{ fontSize: 18, color: "var(--primary-color)" }} />
{/* 文件图标圆形底托 */}
<div
style={{
width: 36,
height: 36,
borderRadius: "50%",
background: "var(--primary-soft, rgba(22, 119, 255, 0.1))",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<SoundOutlined style={{ fontSize: 16, color: "var(--primary-color)" }} />
</div>
{/* 文件信息 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 13,
fontWeight: 500,
color: "var(--text-primary)",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
lineHeight: 1.4,
}}
>
{file.name}
</div>
<div style={{ fontSize: 11, color: "var(--text-secondary)" }}>
<div
style={{
fontSize: 12,
color: "var(--text-secondary)",
marginTop: 2,
lineHeight: 1.3,
}}
>
{formatFileSize(file.size)}
</div>
</div>
@@ -14,6 +14,8 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
onFileSelect,
onFileRemove,
}) => {
const [isHovered, setIsHovered] = React.useState(false)
return (
<Upload.Dragger
accept={UPLOAD_CONFIG.accept}
@@ -28,25 +30,73 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
showUploadList={false}
disabled={disabled}
>
<p
<div
style={{
fontSize: 32,
color: "var(--primary-color)",
marginBottom: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "20px 16px",
borderRadius: 12,
background: isHovered
? "var(--primary-soft, rgba(22, 119, 255, 0.08))"
: "var(--primary-soft, rgba(22, 119, 255, 0.03))",
border: `2px dashed ${isHovered ? "var(--primary-color)" : "var(--border-color)"}`,
transition: "all 0.25s ease",
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.6 : 1,
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<UploadOutlined />
</p>
<p style={{ fontSize: 14, fontWeight: 500, margin: "0 0 4px" }}></p>
<p
style={{
fontSize: 12,
color: "var(--text-secondary)",
margin: 0,
}}
>
MP3WAVAACFLAC {UPLOAD_CONFIG.maxSizeMB}MB
</p>
{/* 图标圆形底托 */}
<div
style={{
width: 44,
height: 44,
borderRadius: "50%",
background: "var(--primary-soft, rgba(22, 119, 255, 0.1))",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginBottom: 10,
transition: "transform 0.2s ease",
transform: isHovered ? "scale(1.08)" : "scale(1)",
}}
>
<UploadOutlined
style={{
fontSize: 20,
color: "var(--primary-color)",
}}
/>
</div>
{/* 主文案 */}
<p
style={{
fontSize: 14,
fontWeight: 500,
color: "var(--text-primary)",
margin: "0 0 6px",
lineHeight: 1.4,
}}
>
</p>
{/* 副文案 */}
<p
style={{
fontSize: 12,
color: "var(--text-secondary)",
margin: 0,
lineHeight: 1.4,
}}
>
MP3WAVAACFLAC {UPLOAD_CONFIG.maxSizeMB}MB
</p>
</div>
</Upload.Dragger>
)
}
@@ -1,101 +1,91 @@
import React from "react"
import type { VoiceGender } from "@/pages/voices/types"
import { GENDER_OPTIONS, UPLOAD_CONFIG } from "./types"
import { UPLOAD_CONFIG } from "./types"
interface FormFieldsProps {
name: string
gender: VoiceGender
desc: string
disabled: boolean
onNameChange: (name: string) => void
onGenderChange: (gender: VoiceGender) => void
onDescChange: (desc: string) => void
}
const labelStyle: React.CSSProperties = {
fontSize: 13,
color: "var(--text-secondary)",
fontSize: 12,
fontWeight: 600,
color: "var(--text-primary, #333)",
marginBottom: 6,
letterSpacing: "0.02em",
}
const inputStyle: React.CSSProperties = {
const baseInputStyle: React.CSSProperties = {
width: "100%",
padding: "8px 12px",
padding: "10px 12px",
border: "1px solid var(--border-color)",
borderRadius: 8,
fontSize: 13,
background: "var(--bg-primary)",
color: "var(--text-primary)",
outline: "none",
transition: "border-color 0.2s ease, box-shadow 0.2s ease",
lineHeight: 1.5,
}
const focusStyle: React.CSSProperties = {
borderColor: "var(--primary-color)",
boxShadow: "0 0 0 2px var(--primary-soft, rgba(22, 119, 255, 0.12))",
}
const FormFields: React.FC<FormFieldsProps> = ({
name,
gender,
desc,
disabled,
onNameChange,
onGenderChange,
onDescChange,
}) => {
return (
<>
{/* 名称 */}
<div>
<div style={labelStyle}></div>
<div style={labelStyle}></div>
<input
value={name}
onChange={(e) => onNameChange(e.target.value)}
placeholder="输入素材名称"
placeholder="输入配音名称"
maxLength={UPLOAD_CONFIG.maxNameLength}
disabled={disabled}
style={inputStyle}
style={baseInputStyle}
onFocus={(e) => {
Object.assign(e.target.style, focusStyle)
}}
onBlur={(e) => {
e.target.style.borderColor = "var(--border-color)"
e.target.style.boxShadow = "none"
}}
/>
</div>
{/* 性别选择 */}
<div>
<div style={labelStyle}></div>
<div style={{ display: "flex", gap: 8 }}>
{GENDER_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => onGenderChange(opt.value)}
disabled={disabled}
style={{
flex: 1,
padding: "6px 0",
borderRadius: 8,
border: `1px solid ${gender === opt.value ? "var(--primary-color)" : "var(--border-color)"}`,
background: gender === opt.value ? "var(--primary-soft)" : "transparent",
color: gender === opt.value ? "var(--primary-color)" : "var(--text-secondary)",
fontSize: 13,
fontWeight: gender === opt.value ? 600 : 400,
cursor: disabled ? "not-allowed" : "pointer",
transition: "all 0.2s",
}}
>
{opt.label}
</button>
))}
</div>
</div>
{/* 描述 */}
<div>
<div style={labelStyle}></div>
<div style={labelStyle}></div>
<textarea
value={desc}
onChange={(e) => onDescChange(e.target.value)}
placeholder="描述这个音色的特点..."
placeholder="描述这段配音的内容..."
maxLength={UPLOAD_CONFIG.maxDescLength}
rows={2}
disabled={disabled}
style={{
...inputStyle,
...baseInputStyle,
resize: "vertical",
fontFamily: "inherit",
minHeight: 56,
}}
onFocus={(e) => {
Object.assign(e.target.style, focusStyle)
}}
onBlur={(e) => {
e.target.style.borderColor = "var(--border-color)"
e.target.style.boxShadow = "none"
}}
/>
</div>
@@ -6,25 +6,40 @@ interface UploadProgressProps {
const UploadProgress: React.FC<UploadProgressProps> = ({ progress }) => {
return (
<div style={{ textAlign: "center", padding: "8px 0" }}>
<div
style={{
padding: "12px 16px",
background: "var(--bg-secondary)",
borderRadius: 10,
textAlign: "center",
border: "1px solid var(--border-color)",
}}
>
<div
style={{
fontSize: 22,
fontSize: 20,
fontWeight: 700,
color: "var(--primary-color)",
lineHeight: 1.2,
}}
>
{progress}%
</div>
<div style={{ fontSize: 12, color: "var(--text-secondary)" }}>
<div
style={{
fontSize: 12,
color: "var(--text-secondary)",
marginTop: 2,
marginBottom: 10,
}}
>
{progress < 100 ? "上传中..." : "处理中..."}
</div>
<div
style={{
height: 4,
background: "var(--bg-tertiary)",
background: "var(--bg-tertiary, #f0f0f0)",
borderRadius: 2,
marginTop: 8,
overflow: "hidden",
}}
>
@@ -32,7 +47,7 @@ const UploadProgress: React.FC<UploadProgressProps> = ({ progress }) => {
style={{
height: "100%",
width: `${progress}%`,
background: "var(--primary-color)",
background: progress < 100 ? "var(--primary-color)" : "var(--success-color, #52c41a)",
borderRadius: 2,
transition: "width 0.3s ease",
}}
@@ -1,27 +1,17 @@
import type { VoiceGender } from "@/pages/voices/types"
export interface UploadVoiceModalProps {
open: boolean
uploadFile: File | null
uploadName: string
uploadGender: VoiceGender
uploadDesc: string
uploadProgress: number | null
onClose: () => void
onFileSelect: (file: File) => void
onFileRemove: () => void
onNameChange: (name: string) => void
onGenderChange: (gender: VoiceGender) => void
onDescChange: (desc: string) => void
onUpload: () => void
}
export const GENDER_OPTIONS: { value: VoiceGender; label: string }[] = [
{ value: "female", label: "女声" },
{ value: "male", label: "男声" },
{ value: "child", label: "童声" },
]
export const UPLOAD_CONFIG = {
maxSizeMB: 200,
maxNameLength: 100,
@@ -1,6 +1,5 @@
import { useState, useCallback } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { type VoiceGender } from "../types"
import { uploadAssetDirect, getAssetLibraries, createAsset } from "@/api/assets"
import { getAudioDuration } from "../utils/audio"
import { buildVoiceMetadata } from "../types"
@@ -19,17 +18,11 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
const [uploadOpen, setUploadOpen] = useState(false)
const [uploadFile, setUploadFile] = useState<File | null>(null)
const [uploadName, setUploadName] = useState("")
const [uploadGender, setUploadGender] = useState<VoiceGender>("female")
const [uploadDesc, setUploadDesc] = useState("")
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
const uploadMutation = useMutation({
mutationFn: async (data: {
file: File
name: string
gender: VoiceGender
description: string
}) => {
mutationFn: async (data: { file: File; name: string; description: string }) => {
setUploadProgress(0)
try {
/* 获取或创建默认配音库 */
@@ -57,7 +50,6 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
storage_key,
mime_type: data.file.type || "audio/mpeg",
metadata: buildVoiceMetadata({
gender: data.gender,
description: data.description,
duration,
}),
@@ -71,8 +63,15 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
showToast("上传成功", "success")
handleUploadClose()
},
onError: (err: Error) => {
showToast(err.message || "上传失败,请重试", "error")
onError: (err: Error & { response?: { status?: number } }) => {
const status = err?.response?.status
let msg = "上传失败,请重试"
if (status && status >= 500) {
msg = "服务器暂时不可用,请稍后重试"
} else if (err?.message) {
msg = err.message
}
showToast(msg, "error")
},
})
@@ -102,10 +101,9 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
uploadMutation.mutate({
file: uploadFile,
name: uploadName.trim(),
gender: uploadGender,
description: uploadDesc.trim(),
})
}, [uploadFile, uploadName, uploadGender, uploadDesc, uploadMutation])
}, [uploadFile, uploadName, uploadDesc, uploadMutation])
return {
// 弹窗状态
@@ -114,13 +112,11 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
// 表单状态
uploadFile,
uploadName,
uploadGender,
uploadDesc,
uploadProgress,
isUploading: uploadMutation.isPending,
// Setters
setUploadName,
setUploadGender,
setUploadDesc,
// Handlers
handleFileSelect,
-1
View File
@@ -3,7 +3,6 @@ import { Navigate } from "react-router-dom"
import { useAuthStore } from "@/store/authStore"
/** 受保护的路由组件 */
// eslint-disable-next-line react-refresh/only-export-components
export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated)
const hasAccessToken = Boolean(localStorage.getItem("access_token"))
-5
View File
@@ -26,12 +26,10 @@ import { refreshAccessToken } from "@/api/auth"
import apiClient from "@/api/client"
// 从真实实例取出拦截器回调
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const requestHandlers = (apiClient as any).interceptors.request.handlers as Array<{
fulfilled: (config: unknown) => unknown
rejected: (error: unknown) => unknown
}>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const responseHandlers = (apiClient as any).interceptors.response.handlers as Array<{
fulfilled: (response: unknown) => unknown
rejected: (error: unknown) => Promise<unknown>
@@ -258,7 +256,6 @@ describe("apiClient - 401 token refresh", () => {
isAuthenticated: false,
clearAuth: mockClearAuth,
setAuth: vi.fn(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
const err = makeAxiosError(401, { detail: "Unauthorized" })
@@ -276,7 +273,6 @@ describe("apiClient - 401 token refresh", () => {
isAuthenticated: true,
clearAuth: vi.fn(),
setAuth: mockSetAuth,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
vi.mocked(refreshAccessToken).mockResolvedValue({
access_token: "new-access",
@@ -306,7 +302,6 @@ describe("apiClient - 401 token refresh", () => {
isAuthenticated: true,
clearAuth: mockClearAuth,
setAuth: vi.fn(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
vi.mocked(refreshAccessToken).mockRejectedValue(new Error("refresh failed") as never)
+1 -1
View File
@@ -107,7 +107,7 @@ describe("useAuth hooks", () => {
})
expect(mockSetAuth).toHaveBeenCalled()
expect(mockNavigate).toHaveBeenCalledWith("/", { replace: true })
expect(mockNavigate).toHaveBeenCalledWith("/app/dashboard", { replace: true })
})
it("登录成功后跳转到 login_redirect 指定的页面", async () => {
@@ -179,6 +179,7 @@ vi.mock("@/api/assets", () => ({
createAssetLibrary: vi.fn().mockResolvedValue({}),
getAssets: vi.fn().mockResolvedValue({ items: [], total: 0 }),
getAssetsByKind: vi.fn().mockResolvedValue({ items: [], total: 0 }),
smartMatchAssets: vi.fn().mockResolvedValue({ items: [] }),
createAsset: vi.fn().mockResolvedValue({}),
updateAsset: vi.fn().mockResolvedValue({}),
deleteAsset: vi.fn().mockResolvedValue({}),
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from "vitest"
import { render } from "@testing-library/react"
import { afterEach, describe, expect, it, vi } from "vitest"
import { render, cleanup } from "@testing-library/react"
import TtsPanel from "@/pages/editing-planner/components/TtsPanel"
import { DEFAULT_TTS_CONFIG } from "@/pages/editing-planner/types"
@@ -16,6 +16,10 @@ const defaultProps = {
}
describe("TtsPanel", () => {
afterEach(() => {
cleanup()
})
it("should render without crashing", () => {
const { container } = render(<TtsPanel {...defaultProps} />)
expect(container).toBeTruthy()
@@ -17,7 +17,7 @@ import "@/api/generation/types"
// 直接引入所有 Step 组件,建立完整依赖链
import "@/pages/generate/GeneratePage"
import "@/pages/generate/components/Step2MaterialSelect"
import "@/pages/generate/components/Step3GeneratePreview"
import "@/pages/generate/components/Step4GeneratePreview"
import "@/pages/generate/components/Step4TitleSettings"
import "@/pages/generate/components/Step5VoiceSelect"
import "@/pages/generate/components/PreviewVideoPanel"
@@ -47,7 +47,7 @@ describe("GeneratePage module smoke test", () => {
})
})
import "@/pages/generate/hooks/useGenerateVideo"
import "@/pages/generate/hooks/useStep3Preview"
import "@/pages/generate/hooks/useStep4Preview"
import "@/pages/generate/hooks/generate-video/useGenerationPolling"
import "@/pages/generate/hooks/useGenerateFormState"
import "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection"
@@ -1,17 +0,0 @@
/**
* Step3GeneratePreview smoke test
* 确保 vitest related 模式能匹配到第3步预览生成相关文件的改动
*/
import { describe, it, expect } from "vitest"
import "@/pages/generate/components/Step3GeneratePreview"
import "@/pages/generate/hooks/useStep3Preview"
import "@/pages/generate/hooks/useStepNavigation"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/GeneratePage"
describe("Step3GeneratePreview module smoke test", () => {
it("should load all step3 preview modules", () => {
expect(true).toBe(true)
})
})
@@ -0,0 +1,17 @@
/**
* Step4GeneratePreview smoke test
* 确保 vitest related 模式能匹配到第4步预览生成相关文件的改动
*/
import { describe, it, expect } from "vitest"
import "@/pages/generate/components/Step4GeneratePreview"
import "@/pages/generate/hooks/useStep4Preview"
import "@/pages/generate/hooks/useStepNavigation"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/GeneratePage"
describe("Step4GeneratePreview module smoke test", () => {
it("should load all step4 preview modules", () => {
expect(true).toBe(true)
})
})
-2
View File
@@ -32,9 +32,7 @@ afterEach(() => {
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Vi {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface Assertion<T = any> extends jest.Matchers<void, T> {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
interface AsymmetricMatchersContaining extends jest.Matchers<void, any> {}
}
}
+38 -30
View File
@@ -465,7 +465,7 @@ class RenderAdapter:
失败不阻断主流程,返回 None。
"""
try:
from services.asr_service_factory import get_asr_service
from apps.worker.services.asr_service_factory import get_asr_service
return get_asr_service()
except Exception as e:
@@ -485,6 +485,7 @@ class RenderAdapter:
rendered_clip_ids: list[str] | None = None,
failed_clip_ids: list[str] | None = None,
voiceover_audio_path: str | None = None,
is_preview: bool = False,
) -> RenderAdapterResult:
"""执行统一渲染核心流程(BGM + ASR + 渲染 + 缩略图 + 上传)。
@@ -504,10 +505,12 @@ class RenderAdapter:
self._report_progress(progress_cb, 40.0, "执行视频渲染")
# 2. 初始化 ASR
asr_service = self._get_asr_service()
# 预览模式下,如果 plan.config 中存在 voice_id,仍需初始化 ASR 以支持配音
plan_config = plan.config or {}
has_voice_id = bool(plan_config.get("voice_id"))
asr_service = None if (is_preview and not has_voice_id) else self._get_asr_service()
# 3. 读取输出分辨率
plan_config = plan.config or {}
export_config = plan_config.get("export", {}) or {}
output_width, output_height = _parse_resolution(export_config.get("resolution"))
logger.info(
@@ -529,25 +532,27 @@ class RenderAdapter:
bgm_path=bgm_path,
asr_service=asr_service,
voiceover_audio_path=voiceover_audio_path,
is_preview=is_preview,
)
result = render_svc.render()
# 4.5 渲染后校验输出完整性
validation = validate_video_output(result.output_path)
if not validation.valid:
logger.error(
"[render-adapter] 渲染输出校验失败: plan_id=%s job_id=%s error=%s",
plan_id,
job_id,
validation.error_message,
)
return RenderAdapterResult(
success=False,
error_message=f"渲染输出校验失败: {validation.error_message}",
error_detail=validation.error_message,
)
# 4.5 渲染后校验输出完整性(预览模式跳过,节省耗时)
if is_preview:
logger.info("[render-adapter] 预览模式:跳过输出校验")
else:
validation = validate_video_output(result.output_path)
if not validation.valid:
logger.error(
"[render-adapter] 渲染输出校验失败: plan_id=%s job_id=%s error=%s",
plan_id,
job_id,
validation.error_message,
)
return RenderAdapterResult(
success=False,
error_message=f"渲染输出校验失败: {validation.error_message}",
error_detail=validation.error_message,
)
self._report_progress(progress_cb, 80.0, "上传渲染结果")
# 5. 上传结果
@@ -556,19 +561,20 @@ class RenderAdapter:
self._report_progress(progress_cb, 90.0, "生成封面缩略图")
# 6. 生成缩略图
# 6. 生成缩略图(预览模式跳过,节省耗时)
thumbnail_url = ""
try:
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
if not is_preview:
try:
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
thumb_storage_key = f"rendered/{plan_id}/thumbnail.jpg"
thumbnail_url = generate_and_upload_thumbnail(str(result.output_path), thumb_storage_key)
except Exception as thumb_err:
logger.warning(
"[render-adapter] 缩略图生成失败(不影响主流程): plan_id=%s error=%s",
plan_id,
thumb_err,
)
thumb_storage_key = f"rendered/{plan_id}/thumbnail.jpg"
thumbnail_url = generate_and_upload_thumbnail(str(result.output_path), thumb_storage_key)
except Exception as thumb_err:
logger.warning(
"[render-adapter] 缩略图生成失败(不影响主流程): plan_id=%s error=%s",
plan_id,
thumb_err,
)
self._report_progress(progress_cb, 100.0, "渲染完成")
@@ -614,6 +620,7 @@ class RenderAdapter:
work_dir: Path | None = None,
progress_cb: ProgressCallback | None = None,
voiceover_audio_path: str | None = None,
is_preview: bool = False,
) -> RenderAdapterResult:
"""使用内存中的 plan/clips/asset_path_map 直接渲染。
@@ -672,6 +679,7 @@ class RenderAdapter:
job_id=job_id,
progress_cb=progress_cb,
voiceover_audio_path=voiceover_audio_path,
is_preview=is_preview,
)
except subprocess.CalledProcessError as exc:
+22 -13
View File
@@ -82,14 +82,12 @@ def mix_audio(
"""音频后处理混音.
处理逻辑:
1. 主音频源按优先级查找:main > brollbackground 不参与主音频,通常是图片无音轨)
2. 主图层音频按顺序 concat 拼接
3. 独立音频轨(audio role)用 amix 混入
4. 输出时长截断到 video_duration
5. 无音频流的 clip 会被自动跳过,避免 FFmpeg 引用 [i:a] 失败
6. 如果提供了 bgm_path,则额外混入 BGM(支持淡入淡出、循环、人声闪避)
7. 如果配置了 audio_tracks,则混入多轨道音频(配音、音效等)
8. 如果配置了降噪,最后应用降噪
1. 丢弃主图层(main/broll/overlay/corner_voice)的原始音频,避免录入源视频杂音
2. 仅使用独立音频轨(audio role,TTS/配音)作为主音频
3. 如果提供了 bgm_path,则额外混入 BGM(支持淡入淡出、循环、人声闪避)
4. 如果配置了 audio_tracks,则混入多轨道音频(配音、音效等)
5. 输出时长截断到 video_duration
6. 如果配置了降噪,最后应用降噪
Args:
ctx: 渲染上下文
@@ -125,8 +123,10 @@ def mix_audio(
if "audio" in layer_map:
audio_clips = layer_map["audio"].clips
# ── 丢弃源视频的原始音频(避免录入杂音),成片仅保留 TTS 配音 + BGM ──
main_clips = []
# ── 防御:过滤掉无音频流的 clip ──
main_clips = [c for c in main_clips if clip_has_audio(ctx, c)]
audio_clips = [c for c in audio_clips if clip_has_audio(ctx, c)]
if not main_clips and not audio_clips:
@@ -144,12 +144,21 @@ def mix_audio(
# 构建音频处理命令
output_path = ctx.work_dir / f"audio_{ctx.plan_id}.aac"
# 简单场景:只有主图层 + 无独立音频 → 直接从视频提取音频并拼接
if main_clips and not audio_clips:
concat_main_audio(ctx, main_clips, output_path, video_duration)
# 源视频原始音频已被丢弃(main_clips = []),最终音频完全由独立音频轨 + BGM + 多轨配置组成。
# 当无 main_clips 时,将独立音频轨作为主音频走 concat 拼接;当二者均有则走 amix 混音。
if main_clips:
effective_main = main_clips
effective_audio = audio_clips
else:
effective_main = audio_clips
effective_audio = []
# 简单场景:只有主音频 + 无独立音频 → 直接拼接
if effective_main and not effective_audio:
concat_main_audio(ctx, effective_main, output_path, video_duration)
else:
# 有独立音频轨 → amix 混音
mix_with_independent_audio(ctx, main_clips, audio_clips, output_path, video_duration)
mix_with_independent_audio(ctx, effective_main, effective_audio, output_path, video_duration)
# ── BGM 混音 ──
if bgm_path and bgm_config and bgm_config.get("enabled", False):
@@ -151,6 +151,7 @@ class UnifiedRenderService:
asr_service: Any = None, # ASRService 实例,用于自动生成字幕
bgm_path: str | None = None, # BGM 本地文件路径
voiceover_audio_path: str | None = None, # 配音素材库音频本地路径
is_preview: bool = False, # 预览模式:ultrafast 编码 + 跳过非必要步骤
):
self.plan = plan
self.clips = clips
@@ -163,6 +164,7 @@ class UnifiedRenderService:
self.asr_service = asr_service
self.bgm_path = bgm_path
self.voiceover_audio_path = voiceover_audio_path
self.is_preview = is_preview
self._transition_engine = TransitionEngine(default_duration=transition_duration)
self._speed_engine = SpeedEngine()
self._asr_timeline_cache: Any = None # ASR 字幕结果缓存,避免重复调用
@@ -677,20 +679,40 @@ class UnifiedRenderService:
len(top_text),
)
# 方式Bvoice_id + 自动字幕 → 字幕对齐配音(预设配音模式)
elif top_voice_id and subtitle_cfg.get("auto_generated", False) and self.asr_service is not None:
tts_cfg = {
"enabled": True,
"voice_id": top_voice_id,
"text": "",
"align_mode": "subtitle",
"overlap_mode": "replace",
}
use_subtitle_align = True
logger.info(
"[unified-render] 检测到预设配音+自动字幕,使用字幕对齐模式: plan_id=%s voice_id=%s",
self.plan.id,
top_voice_id,
)
# ASR 可用时走字幕对齐模式,不可用时降级为整段配音
elif top_voice_id and subtitle_cfg.get("auto_generated", False):
if self.asr_service is not None:
# 字幕对齐模式
tts_cfg = {
"enabled": True,
"voice_id": top_voice_id,
"text": "",
"align_mode": "subtitle",
"overlap_mode": "replace",
}
use_subtitle_align = True
logger.info(
"[unified-render] 检测到预设配音+自动字幕,使用字幕对齐模式: plan_id=%s voice_id=%s",
self.plan.id,
top_voice_id,
)
else:
# 降级:整段配音(预览模式 ASR 不可用时)
# 拼接字幕文本作为配音内容
subtitle_text_content = subtitle_cfg.get("text", "") or ""
tts_cfg = {
"enabled": True,
"voice_id": top_voice_id,
"text": subtitle_text_content,
"align_mode": "full",
"overlap_mode": "replace",
}
logger.info(
"[unified-render] ASR 不可用,降级为整段配音模式: plan_id=%s voice_id=%s text_len=%d",
self.plan.id,
top_voice_id,
len(subtitle_text_content),
)
# 兼容前端顶层字段:voice_id / custom_text / voice_clone_profile_id
# 前端一键生成页面传 config.voice_id + config.custom_text
@@ -1228,9 +1250,9 @@ class UnifiedRenderService:
"-c:v",
"libx264",
"-crf",
"23",
"28" if self.is_preview else "23",
"-preset",
"medium",
"ultrafast" if self.is_preview else "medium",
"-pix_fmt",
"yuv420p",
"-movflags",
@@ -1742,9 +1764,9 @@ class UnifiedRenderService:
"-c:v",
"libx264",
"-crf",
"23",
"28" if self.is_preview else "23",
"-preset",
"medium",
"ultrafast" if self.is_preview else "medium",
"-pix_fmt",
"yuv420p",
"-movflags",
@@ -1753,10 +1775,11 @@ class UnifiedRenderService:
]
logger.info(
"执行渲染: plan_id=%s inputs=%d output=%s",
"执行渲染: plan_id=%s inputs=%d output=%s preview=%s",
self.plan.id,
input_args.count("-i"),
output_path,
self.is_preview,
)
try:
run_ffmpeg(command)
@@ -163,6 +163,36 @@ def _compose_with_unified_engine(task, job_service, job, plan_id: str, db) -> di
job_service.fail_job(job_id, error_msg[:500])
raise RuntimeError(result.error_message)
# 生成封面(如果配置启用)
cover_url = None
try:
from video_processing.cover_generator import generate_cover_from_plan
from packages.adapters.sqlalchemy_impl.edit_plan_repository import (
SQLAlchemyEditPlanRepository as EditPlanRepository,
)
# 获取 plan 对象
plan_repo = EditPlanRepository(db)
plan = plan_repo.get(plan_id)
if plan and result.output_path:
# 检查 cover_config
cover_config = (plan.config or {}).get("cover_config")
if cover_config and cover_config.get("enabled", False):
from pathlib import Path
output_dir = Path(result.output_path).parent
cover_path = generate_cover_from_plan(plan, result.output_path, output_dir)
if cover_path:
# 生成 cover_url(相对路径或上传到存储)
cover_url = f"/covers/{plan_id}.jpg"
logger.info("封面生成成功: plan_id=%s cover_path=%s", plan_id, cover_path)
else:
logger.info("封面生成未启用: plan_id=%s", plan_id)
except Exception as e:
logger.warning("封面生成失败(不影响视频合成): plan_id=%s error=%s", plan_id, e)
# 更新 Job 状态为完成
result_data = {
"plan_id": plan_id,
@@ -175,6 +205,7 @@ def _compose_with_unified_engine(task, job_service, job, plan_id: str, db) -> di
"width": result.width,
"height": result.height,
"file_size": result.file_size,
"cover_url": cover_url,
}
job_service.complete_job(job_id, result=result_data)
+113 -82
View File
@@ -235,10 +235,8 @@ def _build_plan_and_clips_from_task(
"""根据模式和下载的素材路径,构建虚拟 plan + clips + asset_path_map。
模式 → clip_type 映射:
ONE_TAKE: N 个 main clips
PIP: 1 main + N-1 overlay
ONE_TAKE: N 个 main clips(默认,pip/voice_pip 已统一映射为此模式)
VOICE_OVER: N 个 main(config.role=b_roll)
VOICE_PIP: 1 background + 1 corner_voice + N-2 b_roll
Returns:
(virtual_plan, virtual_clips, asset_path_map)
@@ -255,23 +253,14 @@ def _build_plan_and_clips_from_task(
path_to_asset_id[p] = asset_id
path_duration[p] = probe_duration(p)
# 产品已确认全面下线画中画,pip/voice_pip统一走one_take(顺序拼接)
if mode in ("pip", "voice_pip"):
logger.info("画中画模式已下线,%s 强制映射为 one_take", mode)
mode = "one_take"
clips: list[_VirtualClip] = []
if mode == "pip":
# 1 main + N-1 overlay
for i, p in enumerate(downloaded_paths):
clip_type = "main" if i == 0 else "overlay"
clips.append(
_VirtualClip(
id=f"vc_{i:03d}",
plan_id=task_id,
clip_type=clip_type,
order=i,
asset_id=path_to_asset_id[p],
duration=path_duration[p],
)
)
elif mode == "voice_over":
if mode == "voice_over":
# N 个 main(config.role=b_roll)
for i, p in enumerate(downloaded_paths):
clips.append(
@@ -285,25 +274,6 @@ def _build_plan_and_clips_from_task(
config={"role": "b_roll"},
)
)
elif mode == "voice_pip":
# 1 background + 1 corner_voice + N-2 b_roll
for i, p in enumerate(downloaded_paths):
if i == 0:
clip_type = "background"
elif i == 1:
clip_type = "corner_voice"
else:
clip_type = "b_roll"
clips.append(
_VirtualClip(
id=f"vc_{i:03d}",
plan_id=task_id,
clip_type=clip_type,
order=i,
asset_id=path_to_asset_id[p],
duration=path_duration[p],
)
)
else:
# ONE_TAKE (default): N 个 main clips
for i, p in enumerate(downloaded_paths):
@@ -773,7 +743,8 @@ def _download_library_assets(
len(asset_ids),
)
downloaded: list[Path] = []
# 构建待下载列表 (index, asset, storage_key, local_file)
download_jobs: list[tuple[int, Any, str, Path]] = []
failed_assets: list[str] = []
for i, asset in enumerate(assets):
storage_key = asset.file_url if asset.file_url else None
@@ -802,52 +773,81 @@ def _download_library_assets(
ext = Path(storage_key).suffix or ".mp4"
local_file = temp_path / f"asset_{i:03d}_{asset.id}{ext}"
asset_start = time.monotonic()
download_ok = download_asset(storage_key, local_file)
asset_elapsed = time.monotonic() - asset_start
download_jobs.append((i, asset, storage_key, local_file))
if download_ok:
file_size = local_file.stat().st_size if local_file.exists() else 0
downloaded.append(local_file)
logger.info(
"[task_id=%s] Downloaded asset: %s -> %s (size=%d, time=%.1fs)",
task_id,
asset.name,
local_file,
file_size,
asset_elapsed,
)
if gen_task:
gen_task.append_log(
"下载素材",
f"下载成功: {asset.name}",
asset_id=asset.id,
asset_name=asset.name,
success=True,
file_size=file_size,
duration=round(asset_elapsed, 2),
# 并行下载素材(线程池,IO 密集型)
downloaded: list[Path] = []
if download_jobs:
from concurrent.futures import ThreadPoolExecutor, as_completed
max_workers = min(len(download_jobs), 6)
logger.info(
"[task_id=%s] 并行下载素材: count=%d, workers=%d",
task_id,
len(download_jobs),
max_workers,
)
def _download_one(item: tuple) -> tuple[int, Any, Path, bool, float]:
idx, asset, skey, lfile = item
t0 = time.monotonic()
ok = download_asset(skey, lfile)
elapsed = time.monotonic() - t0
return idx, asset, lfile, ok, elapsed
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(_download_one, job): job for job in download_jobs}
# 按原始顺序收集结果,保证 downloaded 列表顺序稳定
results_map: dict[int, tuple[Path, bool, float, Any]] = {}
for future in as_completed(futures):
idx, asset, lfile, ok, elapsed = future.result()
results_map[idx] = (lfile, ok, elapsed, asset)
# 按原始顺序处理结果
for idx in sorted(results_map.keys()):
lfile, ok, elapsed, asset = results_map[idx]
if ok:
file_size = lfile.stat().st_size if lfile.exists() else 0
downloaded.append(lfile)
logger.info(
"[task_id=%s] Downloaded asset: %s -> %s (size=%d, time=%.1fs)",
task_id,
asset.name,
lfile,
file_size,
elapsed,
)
else:
failed_assets.append(f"{asset.name}({asset.id})")
logger.warning(
"[task_id=%s] Failed to download asset: %s (id=%s)",
task_id,
asset.name,
asset.id,
)
if gen_task:
gen_task.append_log(
"下载素材",
f"下载失败: {asset.name}",
level="WARN",
asset_id=asset.id,
asset_name=asset.name,
success=False,
file_size=0,
duration=round(asset_elapsed, 2),
if gen_task:
gen_task.append_log(
"下载素材",
f"下载成功: {asset.name}",
asset_id=asset.id,
asset_name=asset.name,
success=True,
file_size=file_size,
duration=round(elapsed, 2),
)
else:
failed_assets.append(f"{asset.name}({asset.id})")
logger.warning(
"[task_id=%s] Failed to download asset: %s (id=%s)",
task_id,
asset.name,
asset.id,
)
if strict:
raise RuntimeError(f"素材下载失败: asset_id={asset.id}, name={asset.name}")
if gen_task:
gen_task.append_log(
"下载素材",
f"下载失败: {asset.name}",
level="WARN",
asset_id=asset.id,
asset_name=asset.name,
success=False,
file_size=0,
duration=round(elapsed, 2),
)
if strict:
raise RuntimeError(f"素材下载失败: asset_id={asset.id}, name={asset.name}")
# 指定了 asset_ids 但全部下载失败 → 无论 strict 与否都报错
if asset_ids and not downloaded:
@@ -1040,6 +1040,12 @@ def _load_task_info(task_id: str) -> dict | None:
"resolution": getattr(gen_task, "resolution", "") or "",
"bgm_config": dict(getattr(gen_task, "bgm_config", {}) or {}),
"is_preview": bool(getattr(gen_task, "is_preview", False)),
"source_task_id": getattr(gen_task, "source_task_id", "") or "",
"output_width": getattr(gen_task, "output_width", OUTPUT_WIDTH) or OUTPUT_WIDTH,
"output_height": getattr(gen_task, "output_height", OUTPUT_HEIGHT) or OUTPUT_HEIGHT,
"cover_url": getattr(gen_task, "cover_url", "") or "",
"custom_title": getattr(gen_task, "custom_title", "") or "",
"voice_ids": list(getattr(gen_task, "voice_ids", []) or []),
}
finally:
session.close()
@@ -1101,6 +1107,7 @@ def _render_video(
resolution: str = "",
bgm_config: dict | None = None,
is_preview: bool = False,
voice_ids: list[str] | None = None,
) -> tuple[Path, float]:
"""渲染视频(含配音混音)。
@@ -1175,6 +1182,20 @@ def _render_video(
plan_cfg["export"] = export_cfg
virtual_plan.config = plan_cfg
# 注入用户选择的配音 voice_id(ASR 字幕对齐模式)
if voice_ids:
plan_cfg = dict(virtual_plan.config or {})
plan_cfg["voice_id"] = voice_ids[0]
subtitle_cfg = plan_cfg.get("subtitle", {}) or {}
subtitle_cfg["auto_generated"] = True
plan_cfg["subtitle"] = subtitle_cfg
virtual_plan.config = plan_cfg
logger.info(
"[task_id=%s] [渲染] 预览配音已注入: voice_id=%s",
task_id,
voice_ids[0],
)
total_duration = sum(c.duration for c in virtual_clips)
logger.info(
"[task_id=%s] [剪辑计划] 片段数=%d, 总时长=%.1fs",
@@ -1201,6 +1222,7 @@ def _render_video(
job_id=task_id,
work_dir=temp_path,
voiceover_audio_path=voice_path,
is_preview=is_preview,
)
finally:
db.close()
@@ -1414,6 +1436,14 @@ def generate_video(self, task_id: str) -> dict:
# ── 3. 渲染 + 混音 ───────────────────────────────────────────────
_update_task_progress(task_id, 40, "开始渲染")
# 动态分辨率:优先使用 output_width/output_height,其次 resolution 字符串
_ow = task_info.get("output_width", OUTPUT_WIDTH) or OUTPUT_WIDTH
_oh = task_info.get("output_height", OUTPUT_HEIGHT) or OUTPUT_HEIGHT
if _ow != OUTPUT_WIDTH or _oh != OUTPUT_HEIGHT:
_resolved_resolution = f"{_ow}x{_oh}"
else:
_resolved_resolution = task_info.get("resolution", "")
output_path, render_duration = _render_video(
task_id=task_id,
downloaded_videos=downloaded_videos,
@@ -1424,9 +1454,10 @@ def generate_video(self, task_id: str) -> dict:
user_id=user_id,
temp_path=temp_path,
output_name=output_name,
resolution=task_info.get("resolution", ""),
resolution=_resolved_resolution,
bgm_config=task_info.get("bgm_config", {}),
is_preview=task_info.get("is_preview", False),
voice_ids=task_info.get("voice_ids", []),
)
if gen_task:
+208
View File
@@ -0,0 +1,208 @@
# #1197 预览生成接口方案评估
## 背景
智能剪辑「一键生成」流程中,第3步预览生成当前被跳过,直接进入下一步。需要实现真正的预览生成功能,让用户在正式生成前能看到效果预览。
## 现状分析
### 现有生成链路
```
API 触发生成 → GenerationTask入库 → Celery异步任务 → UnifiedRenderService渲染 → OSS上传 → 更新状态
```
**关键节点:**
1. **API层**`POST /generation-tasks``POST /templates/{id}/generate` 触发生成
2. **任务调度**Celery task `worker.generate_video`
3. **渲染引擎**`UnifiedRenderService`(统一渲染引擎,已接入9个效果层)
4. **输出配置**:默认 720p (1280x720),支持 `resolution` 字段自定义
5. **产物存储**`GeneratedVideo` 表记录,OSS 存储视频文件
### 已有可复用能力
| 能力 | 位置 | 是否可复用 |
|------|------|-----------|
| 任务创建与状态管理 | `GenerationTask` + `CreateGenerationTaskUseCase` | ✅ 是 |
| 素材下载与预处理 | `_download_video_assets` / `_download_voice_asset` | ✅ 是 |
| 统一渲染引擎 | `UnifiedRenderService` | ✅ 是 |
| 分辨率配置 | `resolution` 字段已支持 | ✅ 是 |
| 混音与后处理 | `_render_video` 内流程 | ✅ 是 |
| OSS 上传与查重 | `_upload_and_dedup` | ✅ 是 |
| 进度追踪 | `append_log` / `progress` 字段 | ✅ 是 |
## 方案对比
### 方案A:复用现有生成链路 + is_preview 标记(推荐)
**思路**:在现有 GenerationTask 上加 `is_preview` 标记,预览生成走完整链路但参数降级。
**改动点:**
1. **数据模型**`GenerationTask``is_preview: bool` 字段(默认 false);`GeneratedVideo``is_preview: bool`
2. **API 层**:生成接口加 `is_preview` 参数,预览任务不计入配额
3. **渲染参数**:预览模式下自动调整
- 分辨率:480p (854x480)
- 时长:限制前 15 秒(或模板第一个片段)
- 码率:降低至 1.5Mbps(正式 4Mbps
- 效果层:跳过高级转场/粒子特效等耗时效果
4. **任务调度**:预览任务走低优先级队列(或复用现有队列,标记优先级)
5. **前端对接**:预览生成结果带 `is_preview=true` 标记,前端展示"预览"标签
**优点:**
- 代码复用率 90%+,改动最小
- 与正式生成逻辑一致,预览效果真实可信
- 进度查询、结果展示等功能直接复用
- 后续可平滑升级:预览满意后一键转正式生成
**缺点:**
- 需要区分预览和正式任务,避免数据混淆
- 预览任务和正式任务竞争同一队列资源(可后续优化为独立队列)
**开发量估算**2-3 天
- 数据模型 + 迁移:0.5 天
- API 层改造:0.5 天
- 渲染参数降级:1 天
- 测试 + 联调:1 天
---
### 方案B:新建独立预览接口 + 轻量渲染逻辑
**思路**:新建独立的预览生成接口,使用简化的渲染逻辑(如只拼接素材+基础配音,跳过大部分效果)。
**改动点:**
1. 新增 `PreviewTask` 数据模型
2. 新增 `POST /api/v1/preview/generate` 接口
3. 新增独立的 Celery task `worker.generate_preview`
4. 简化渲染流程:只做素材裁剪+拼接+配音,跳过转场/滤镜/字幕特效等
**优点:**
- 完全隔离,不影响正式生成链路
- 可以做极致优化,预览生成速度快
- 数据模型清晰,不会混淆
**缺点:**
- 代码重复率高,两套生成逻辑维护成本翻倍
- 预览效果与正式生成可能不一致(效果层差异)
- 前端需要对接两套接口
- 无法从预览升级为正式生成(需重新走完整流程)
**开发量估算**4-5 天
- 数据模型 + 接口:1 天
- 简化渲染逻辑:2 天
- 测试 + 联调:1-2 天
---
### 方案C:图片预览(首帧/关键帧截图)
**思路**:不生成视频,只生成几张关键帧的预览图片。
**优点:**
- 生成速度极快(秒级)
- 资源消耗小
**缺点:**
- 预览效果差,用户无法感知动态效果
- 无法验证配音、转场、节奏等时间维度的效果
- 用户体验不佳,不如"真预览"有说服力
**开发量估算**1-2 天
---
## 推荐方案:方案A(复用现有生成链路)
### 核心理由
1. **效果保真**:预览和正式生成用同一套渲染引擎,效果一致,用户信任度高
2. **开发效率**90% 代码复用,2-3 天可上线
3. **可扩展性强**:后续可加「预览转正式」「低分辨率快速预览」等增强功能
4. **维护成本低**:一套生成逻辑,bug 修复和新功能同时生效
### 详细设计
#### 1. 数据模型变更
```python
# GenerationTask 新增字段
is_preview: bool = False
"""是否为预览生成"""
preview_of: str = ""
"""预览对应的正式任务 ID(或反向关联)"""
# GeneratedVideo 新增字段
is_preview: bool = False
"""是否为预览视频"""
```
**迁移**alembic 新增 migration,两个表各加 1-2 个字段。
#### 2. API 层
```
POST /api/v1/generation-tasks
Body 增加 is_preview: bool = false
POST /api/v1/templates/{id}/generate
Query 增加 is_preview: bool = false
```
**配额处理**:预览生成不计入用户配额,不占用生成次数限制。
#### 3. 渲染参数降级
| 参数 | 正式生成 | 预览生成 |
|------|---------|---------|
| 分辨率 | 720p (1280x720) | 480p (854x480) |
| 码率 | 4 Mbps | 1.5 Mbps |
| 时长 | 完整时长 | 前 15 秒(或第一段) |
| 帧率 | 30 fps | 24 fps |
| 转场效果 | 完整转场 | 仅淡入淡出(或简单切) |
| 特效滤镜 | 全部启用 | 跳过粒子/光效等高级效果 |
| 字幕 | 完整渲染 | 正常渲染(字幕是核心信息) |
| 配音 | 完整混音 | 正常混音(配音是核心信息) |
**实现方式**:在 `_render_video` 或 UnifiedRenderService 入口处,根据 `is_preview` 标记调整渲染配置。
#### 4. 任务调度
- 初期复用现有队列,预览任务正常排队
- 后续如需优化,可拆分独立预览队列(低优先级)
- 预览任务可设置较短超时时间
#### 5. 前端对接
- 调用生成接口时传 `is_preview=true`
- 结果列表中预览视频带「预览」标签
- 预览满意后可一键「升级为正式生成」(重新触发全分辨率生成,可复用素材下载缓存)
### 实施步骤
**Phase 1MVP2天):**
1. 数据模型 + 迁移
2. API 层支持 is_preview 参数
3. 渲染分辨率降级(480p
4. 不计入配额
5. 基础测试
**Phase 2(优化,1-2天):**
1. 时长限制(前15秒)
2. 效果层降级(跳高级效果)
3. 预览任务低优先级队列
4. 预览转正式生成功能
## 与前端对齐点
1. 预览生成的触发时机(第3步自动生成?用户点击才生成?)
2. 预览时长是固定15秒还是完整但低清?
3. 是否需要「预览转正式生成」功能
4. 预览视频的展示形态(和正式视频一样还是有特殊UI)
## 风险与注意事项
1. **数据混淆**:确保统计、计费、列表展示时正确区分预览和正式任务
2. **存储成本**:预览视频也占 OSS 空间,可设置自动清理(7天后自动删除)
3. **用户预期**:要明确告诉用户这是预览,效果和正式生成一致但清晰度低
4. **并发压力**:如果用户频繁生成预览,可能增加系统负载,需要限流
+56 -1
View File
@@ -1659,6 +1659,54 @@
"primary_key": false,
"type": "DATETIME",
"unique": false
},
{
"index": false,
"name": "is_preview",
"nullable": false,
"primary_key": false,
"type": "BOOLEAN",
"unique": false
},
{
"index": true,
"name": "source_task_id",
"nullable": false,
"primary_key": false,
"type": "VARCHAR(32)",
"unique": false
},
{
"index": false,
"name": "output_width",
"nullable": false,
"primary_key": false,
"type": "INTEGER",
"unique": false
},
{
"index": false,
"name": "output_height",
"nullable": false,
"primary_key": false,
"type": "INTEGER",
"unique": false
},
{
"index": false,
"name": "cover_url",
"nullable": false,
"primary_key": false,
"type": "VARCHAR(1000)",
"unique": false
},
{
"index": false,
"name": "custom_title",
"nullable": false,
"primary_key": false,
"type": "VARCHAR(500)",
"unique": false
}
],
"indexes": [
@@ -1717,6 +1765,13 @@
],
"name": "ix_generation_tasks_template_id",
"unique": false
},
{
"columns": [
"source_task_id"
],
"name": "ix_generation_tasks_source_task_id",
"unique": false
}
],
"primary_key": [
@@ -3515,4 +3570,4 @@
]
}
}
}
}
+7 -6
View File
@@ -15,7 +15,7 @@
# API_PORT - API 端口映射 (staging: 8000, production: 8001)
# WEB_PORT - Web 端口映射 (staging: 3001, production: 3002)
# GENERATED_FILES_HOST_DIR - 生成文件的主机目录
# WORKER_CONCURRENCY - Worker 并发数 (默认: 1)
# WORKER_CONCURRENCY - Worker 并发数 (默认: 4)
# WORKER_MAX_TASKS_PER_CHILD - Worker 每个子进程最大任务数 (默认: 100)
#
# 重要:
@@ -109,7 +109,7 @@ services:
environment:
APP_ENV: ${APP_ENV:-staging}
APP_VERSION: ${APP_VERSION:-unknown}
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-1}
WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-4}
WORKER_MAX_TASKS_PER_CHILD: ${WORKER_MAX_TASKS_PER_CHILD:-100}
GENERATED_FILES_DIR: /app/generated
GENERATED_FILES_URL_PREFIX: /generated-files
@@ -136,14 +136,15 @@ services:
# 资源限制建议(生产环境建议启用)
# =========================================
# 注意: Worker 需要处理视频,建议分配更多资源
# 并发 4 时需要 4C8G 以上,确保视频渲染不 OOM
deploy:
resources:
limits:
cpus: '2.0'
memory: 2g
cpus: '4.0'
memory: 8g
reservations:
cpus: '0.5'
memory: 1G
cpus: '1.0'
memory: 2G
# =========================================
# Web 服务(Nginx + 前端静态文件)
+1 -1
View File
@@ -145,7 +145,7 @@ docker run -d \
--network xiaoxia-net-production \
-e APP_ENV=production \
-e APP_VERSION="$IMAGE_TAG" \
-e WORKER_CONCURRENCY=1 \
-e WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-4}" \
-e WORKER_MAX_TASKS_PER_CHILD=100 \
-e GENERATED_FILES_DIR=/app/generated \
-e GENERATED_FILES_URL_PREFIX=/generated-files \
+1 -1
View File
@@ -81,7 +81,7 @@ export WEB_DOCKERFILE=infra/docker/web-artifact.Dockerfile
export WEB_NGINX_CONF=infra/docker/nginx-production.conf
docker network create xiaoxia-net-production 2>/dev/null || true
export WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-1}"
export WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-4}"
export WORKER_MAX_TASKS_PER_CHILD="${WORKER_MAX_TASKS_PER_CHILD:-100}"
if [ "${ALLOW_PRODUCTION_BUILDS:-false}" = "true" ]; then
+1 -1
View File
@@ -108,7 +108,7 @@ docker run -d \
--network xiaoxia-net-staging \
-e APP_ENV=staging \
-e APP_VERSION="$IMAGE_TAG" \
-e WORKER_CONCURRENCY=1 \
-e WORKER_CONCURRENCY="${WORKER_CONCURRENCY:-4}" \
-e WORKER_MAX_TASKS_PER_CHILD=100 \
-e GENERATED_FILES_DIR=/app/generated \
-e GENERATED_FILES_URL_PREFIX=/generated-files \
+3 -4
View File
@@ -11,8 +11,7 @@ WORKDIR /app/apps/web
# 安装依赖:node_modules写入镜像layer,走ACR缓存保证完整性
# /root/.npm 保留cache mount加速下载(不影响构建正确性)
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm config set registry https://registry.npmmirror.com \
&& npm ci
npm ci --registry=https://registry.npmmirror.com
# 再拷源码
COPY apps/web/ ./
@@ -21,8 +20,8 @@ COPY apps/web/ ./
# node_modules直接使用镜像中已安装的(layer缓存保证完整性)
RUN --mount=type=cache,target=/app/apps/web/.tscache,sharing=locked \
mkdir -p .tscache \
&& npx tsc --incremental --tsBuildInfoFile .tscache/tsconfig.tsbuildinfo \
&& npx vite build
&& ./node_modules/.bin/tsc --incremental --tsBuildInfoFile .tscache/tsconfig.tsbuildinfo \
&& ./node_modules/.bin/vite build
# Production stage with nginx
FROM git.xiaoxiajianji.com/xiaoxia/base/nginx:alpine AS runner
@@ -37,6 +37,11 @@ def _to_domain(model: GenerationTaskModel) -> GenerationTask:
resolution=getattr(model, "resolution", "") or "",
bgm_config=dict(getattr(model, "bgm_config", {}) or {}),
is_preview=bool(getattr(model, "is_preview", False)),
source_task_id=getattr(model, "source_task_id", "") or "",
output_width=getattr(model, "output_width", 1280) or 1280,
output_height=getattr(model, "output_height", 720) or 720,
cover_url=getattr(model, "cover_url", "") or "",
custom_title=getattr(model, "custom_title", "") or "",
logs=model.logs or "[]",
created_at=model.created_at,
updated_at=model.updated_at,
@@ -76,6 +81,11 @@ class SQLAlchemyGenerationTaskRepository:
resolution=task.resolution or "",
bgm_config=task.bgm_config or {},
is_preview=task.is_preview or False,
source_task_id=task.source_task_id or "",
output_width=task.output_width,
output_height=task.output_height,
cover_url=task.cover_url or "",
custom_title=task.custom_title or "",
logs=task.logs,
created_at=task.created_at,
updated_at=task.updated_at,
@@ -242,6 +252,11 @@ class SQLAlchemyGenerationTaskRepository:
model.bgm_config = task.bgm_config or {}
if hasattr(model, "is_preview"):
model.is_preview = task.is_preview or False
model.source_task_id = task.source_task_id or ""
model.output_width = task.output_width
model.output_height = task.output_height
model.cover_url = task.cover_url or ""
model.custom_title = task.custom_title or ""
model.logs = task.logs
self.session.commit()
return task
@@ -293,6 +293,11 @@ class GenerationTaskModel(Base):
video_title = Column(String(255), nullable=False, default="")
resolution = Column(String(20), nullable=False, default="")
is_preview = Column(Boolean, nullable=False, default=False, index=True)
source_task_id = Column(String(32), nullable=False, default="", index=True)
output_width = Column(Integer, nullable=False, default=1280)
output_height = Column(Integer, nullable=False, default=720)
cover_url = Column(String(1000), nullable=False, default="")
custom_title = Column(String(500), nullable=False, default="")
bgm_config = Column(JSON, nullable=False, default=dict)
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
logs = Column(Text, nullable=False, default="[]", server_default="[]")
+10
View File
@@ -27,6 +27,11 @@ class CreateGenerationTaskCommand:
auto_retry_enabled: bool = False
auto_retry_max: int = 0
is_preview: bool = False
source_task_id: str = ""
output_width: int = 1280
output_height: int = 720
cover_url: str = ""
custom_title: str = ""
class CreateGenerationTaskUseCase:
@@ -58,6 +63,11 @@ class CreateGenerationTaskUseCase:
auto_retry_enabled=command.auto_retry_enabled,
auto_retry_max=command.auto_retry_max,
is_preview=command.is_preview,
source_task_id=command.source_task_id,
output_width=command.output_width,
output_height=command.output_height,
cover_url=command.cover_url,
custom_title=command.custom_title,
)
return self.generation_task_repository.create(task)
+4
View File
@@ -24,6 +24,7 @@ from .entities import (
from .generated_video import GeneratedVideo
from .generation_task import GenerationTask, GenerationTaskStatus
from .job import Job, JobStatus, JobType
from .smart_match import SmartMatchResult, score_asset, smart_select_assets
from .tag import Tag
from .template_clip_config import ClipType, TemplateClipConfig, TransitionEffect
from .title_library import TitleLibraryItem
@@ -61,6 +62,9 @@ __all__ = [
"TemplateClipConfig",
"TransitionEffect",
"User",
"SmartMatchResult",
"TitleLibraryItem",
"VoiceLibraryItem",
"score_asset",
"smart_select_assets",
]
+47 -1
View File
@@ -175,6 +175,48 @@ def format_ass_time(seconds: float) -> str:
# ── 完整 ASS 内容生成 ─────────────────────────────────────────────────────────
def _wrap_title_text(
text: str,
video_width: int,
font_size: int,
margin_l: int = TITLE_MARGIN_SIDE,
margin_r: int = TITLE_MARGIN_SIDE,
) -> str:
"""根据视频宽度和字号自动换行标题文本。
中文字符按 font_size 像素宽度估算,英文/数字按半角估算。
超过可用宽度时插入 \\N (ASS 硬换行)。
"""
if not text or video_width <= 0 or font_size <= 0:
return text
available_width = video_width - margin_l - margin_r
if available_width <= 0:
return text
lines: list[str] = []
current_line = ""
current_width = 0.0
for ch in text:
# CJK 字符按全角估算,其他按半角
char_width = float(font_size) if ord(ch) > 0x2E80 else font_size * 0.55
if current_width + char_width > available_width and current_line:
lines.append(current_line)
current_line = ch
current_width = char_width
else:
current_line += ch
current_width += char_width
if current_line:
lines.append(current_line)
return "\\N".join(lines)
def build_ass_content(
*,
video_width: int,
@@ -248,7 +290,11 @@ def build_ass_content(
)
)
safe_title_text = escape_ass_text(title_text)
# 根据视频宽度和字号自动换行标题,防止超出画面
# 先 escape 特殊字符,再插入换行符 \N,避免顺序颠倒导致 \N 被转义
title_font_size = int(title_config.get("size", 48))
safe_title_text_raw = escape_ass_text(title_text)
safe_title_text = _wrap_title_text(safe_title_text_raw, video_width, title_font_size)
events.append(
"Dialogue: 0,0:00:00.00," f"{format_ass_time(video_duration)}," "TitleStyle,,0,0,0,," f"{safe_title_text}"
-372
View File
@@ -1,372 +0,0 @@
"""Asset scoring pure logic — multi-dimensional scoring + diverse selection.
从 smart_asset_selector.py 抽出来的纯逻辑模块:
- 评分维度:质量分、分辨率、时长、码率(加权求和,总分 0-1)
- 多样性选择:按时长分桶(短/中/长)保证分布均匀
- 数据类:AssetScoreDetail, SmartSelectResult
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# ── 评分权重(总和 = 1.0) ────────────────────────────────────────────────────
WEIGHT_QUALITY = 0.5
WEIGHT_RESOLUTION = 0.2
WEIGHT_DURATION = 0.2
WEIGHT_BITRATE = 0.1
# ── 评分参数 ──────────────────────────────────────────────────────────────────
TARGET_WIDTH = 1920 # 目标分辨率宽度基准
TARGET_HEIGHT = 1080 # 目标分辨率高度基准
MIN_QUALITY_SCORE = 30.0 # 最低质量分门槛(低于此值的素材直接排除)
OPTIMAL_DURATION_MIN = 3.0 # 最佳时长区间(秒)
OPTIMAL_DURATION_MAX = 30.0
# ── 多样性分桶阈值 ───────────────────────────────────────────────────────────
SHORT_BUCKET_MAX = 5.0 # 短素材:< 5s
MEDIUM_BUCKET_MAX = 15.0 # 中素材:5-15s
# 长素材:>= 15s
# ── 数据类 ───────────────────────────────────────────────────────────────────
@dataclass
class AssetScoreDetail:
"""单个素材的评分详情."""
asset_id: str
total_score: float
quality_score: float
resolution_score: float
duration_score: float
bitrate_score: float
duration: float | None
@dataclass
class SmartSelectResult:
"""智能选择结果."""
selected_ids: list[str]
total_candidates: int
filtered_out: int # 被质量门槛过滤的数量
avg_score: float
details: list[AssetScoreDetail] = field(default_factory=list)
# ── 评分函数 ─────────────────────────────────────────────────────────────────
def score_resolution(
width: int | None,
height: int | None,
target_width: int = TARGET_WIDTH,
target_height: int = TARGET_HEIGHT,
) -> float:
"""分辨率评分:越接近目标分辨率得分越高,低于480p扣分严重.
Args:
width: 素材宽度(像素)
height: 素材高度(像素)
target_width: 目标宽度
target_height: 目标高度
Returns:
0.0 - 1.0 之间的评分
"""
if width is None or height is None or width <= 0 or height <= 0:
return 0.5 # 未知分辨率给中评分
target_pixels = target_width * target_height
actual_pixels = width * height
# 计算像素数比例
ratio = actual_pixels / target_pixels
if ratio >= 1.0:
# 高于或等于目标分辨率:满分,略高不扣分(4K也给满分)
return 1.0
else:
# 低于目标分辨率:线性衰减,但最低不低于 0.1
score = 0.3 + 0.7 * ratio
return max(0.1, min(1.0, score))
def score_duration(duration: float | None) -> float:
"""时长评分:3-30秒最佳,过短或过长都扣分.
Args:
duration: 时长(秒)
Returns:
0.0 - 1.0 之间的评分
"""
if duration is None or duration <= 0:
return 0.5 # 未知时长给中评分
if OPTIMAL_DURATION_MIN <= duration <= OPTIMAL_DURATION_MAX:
# 最佳区间:满分
return 1.0
if duration < OPTIMAL_DURATION_MIN:
# 太短:线性衰减,趋近于 0.3
ratio = duration / OPTIMAL_DURATION_MIN
return 0.3 + 0.7 * ratio
# 太长:每超过最佳区间上限10秒扣 0.1 分,最低 0.2
excess = duration - OPTIMAL_DURATION_MAX
penalty = min(0.8, excess / 10.0 * 0.1)
return max(0.2, 1.0 - penalty)
def score_bitrate(file_size: int, duration: float | None) -> float:
"""码率评分:根据文件大小和时长估算码率,适中得分高.
Args:
file_size: 文件大小(字节)
duration: 时长(秒)
Returns:
0.0 - 1.0 之间的评分
"""
if not file_size or not duration or duration <= 0:
return 0.5 # 未知给中评分
# 估算码率(bps
bitrate = (file_size * 8) / duration
# 最佳码率范围:2-8 Mbps
optimal_low = 2_000_000 # 2 Mbps
optimal_high = 8_000_000 # 8 Mbps
if optimal_low <= bitrate <= optimal_high:
return 1.0
if bitrate < optimal_low:
# 码率太低:线性衰减
ratio = bitrate / optimal_low
return 0.3 + 0.7 * ratio
# 码率太高(文件太大):适度扣分,最低 0.5
excess = bitrate / optimal_high - 1.0
penalty = min(0.5, excess * 0.2)
return max(0.5, 1.0 - penalty)
def calculate_total_score(
quality_score: float,
resolution_score: float,
duration_score: float,
bitrate_score: float,
) -> float:
"""计算加权总分.
Args:
quality_score: 质量分(0-1
resolution_score: 分辨率分(0-1
duration_score: 时长分(0-1
bitrate_score: 码率分(0-1
Returns:
加权总分(0-1
"""
total = (
WEIGHT_QUALITY * quality_score
+ WEIGHT_RESOLUTION * resolution_score
+ WEIGHT_DURATION * duration_score
+ WEIGHT_BITRATE * bitrate_score
)
return round(total, 4)
def score_asset_detail(
asset_id: str,
quality: float | None,
width: int | None,
height: int | None,
duration: float | None,
file_size: int,
target_width: int = TARGET_WIDTH,
target_height: int = TARGET_HEIGHT,
) -> AssetScoreDetail:
"""对单个素材进行多维度评分,返回详细评分结果.
Args:
asset_id: 素材ID
quality: 质量分(0-100None表示未知)
width: 宽度
height: 高度
duration: 时长
file_size: 文件大小
target_width: 目标宽度
target_height: 目标高度
Returns:
AssetScoreDetail 评分详情
"""
# 质量分归一化到 0-1
quality_score = (quality / 100.0) if quality is not None else 0.5
resolution_score = score_resolution(width, height, target_width, target_height)
duration_score = score_duration(duration)
bitrate_score = score_bitrate(file_size, duration)
total_score = calculate_total_score(
quality_score,
resolution_score,
duration_score,
bitrate_score,
)
return AssetScoreDetail(
asset_id=asset_id,
total_score=total_score,
quality_score=round(quality_score, 4),
resolution_score=round(resolution_score, 4),
duration_score=round(duration_score, 4),
bitrate_score=round(bitrate_score, 4),
duration=duration,
)
# ── 多样性选择 ───────────────────────────────────────────────────────────────
def _bucket_by_duration(item: AssetScoreDetail) -> str:
"""根据时长判断所属桶.
Returns:
'short' / 'medium' / 'long' / 'unknown'
"""
if item.duration is None:
return "unknown"
if item.duration < SHORT_BUCKET_MAX:
return "short"
if item.duration < MEDIUM_BUCKET_MAX:
return "medium"
return "long"
def diverse_selection(
scored: list[AssetScoreDetail],
count: int,
) -> list[AssetScoreDetail]:
"""多样性选择:按时长分桶,保证每个桶都有素材.
策略:
1. 按时长分为三桶:短(<5s)、中(5-15s)、长(>=15s)
2. 每个桶配额 = max(1, count // 3)
3. 先从每桶按配额取最高分的
4. 剩余名额从全局最高分中取(不重复)
5. 如果还不够,加上未知时长的
Args:
scored: 已按总分降序排列的评分列表
count: 需要选取的数量
Returns:
选中的评分列表(不超过 count 个)
"""
if count <= 0 or not scored:
return []
# 分桶
short_bucket = [d for d in scored if _bucket_by_duration(d) == "short"]
medium_bucket = [d for d in scored if _bucket_by_duration(d) == "medium"]
long_bucket = [d for d in scored if _bucket_by_duration(d) == "long"]
unknown_bucket = [d for d in scored if _bucket_by_duration(d) == "unknown"]
buckets = [short_bucket, medium_bucket, long_bucket]
# 每个桶基础配额(至少1个,如果桶非空且需要的话)
base_quota = max(1, count // 3)
selected: list[AssetScoreDetail] = []
selected_ids: set[str] = set()
# 先按配额从每个桶取
for bucket in buckets:
quota = min(base_quota, len(bucket))
if quota <= 0:
continue
# 桶内已经按分数排好序了,直接取前 quota 个
for item in bucket[:quota]:
if item.asset_id not in selected_ids:
selected.append(item)
selected_ids.add(item.asset_id)
if len(selected) >= count:
return selected
# 剩余名额:从全局(未被选中的)中按分数高低取
remaining_needed = count - len(selected)
if remaining_needed > 0:
for item in scored:
if item.asset_id not in selected_ids:
selected.append(item)
selected_ids.add(item.asset_id)
if len(selected) >= count:
break
# 如果还不够(不应该发生),加上未知时长的
if len(selected) < count and unknown_bucket:
for item in unknown_bucket:
if item.asset_id not in selected_ids:
selected.append(item)
selected_ids.add(item.asset_id)
if len(selected) >= count:
break
return selected[:count]
# ── 候选过滤 ─────────────────────────────────────────────────────────────────
def filter_candidates(
assets: list[Any],
min_quality_score: float = MIN_QUALITY_SCORE,
) -> tuple[list[Any], int]:
"""从素材列表中筛选出合格的候选素材.
筛选条件:
- status == 'ready'
- mime_type 以 'video' 开头
- quality_score >= min_quality_score(如果quality不为None
Args:
assets: 素材列表
min_quality_score: 最低质量分门槛
Returns:
(合格素材列表, 被质量门槛过滤的数量)
"""
candidates = []
filtered_out = 0
for asset in assets:
# 状态检查
status = getattr(asset, "status", None)
status_val = status.value if hasattr(status, "value") else str(status)
if status_val != "ready":
continue
# 类型检查
mime_type = getattr(asset, "mime_type", "") or ""
if not mime_type.startswith("video"):
continue
# 质量分门槛
quality = getattr(asset, "quality_score", None)
if quality is not None and quality < min_quality_score:
filtered_out += 1
continue
candidates.append(asset)
return candidates, filtered_out
+9 -2
View File
@@ -127,11 +127,18 @@ class EditPlanClip:
config=config or {},
)
def assign_asset(self, asset_id: str) -> None:
"""分配素材"""
def assign_asset(self, asset_id: str, *, start_time: float | None = None) -> None:
"""分配素材
Args:
asset_id: 素材 ID
start_time: 可选,素材播放起始时间(秒)。如果提供且在有效范围内,则设置;否则保持默认 0.0
"""
if not asset_id.strip():
raise ValueError("asset_id 不能为空")
self.asset_id = asset_id.strip()
if start_time is not None and start_time >= 0:
self.start_time = start_time
self.updated_at = datetime.now(timezone.utc)
def mark_ready(self) -> None:
+15
View File
@@ -116,6 +116,11 @@ class GenerationTask:
resolution: str = ""
bgm_config: dict = field(default_factory=dict)
is_preview: bool = False
source_task_id: str = ""
output_width: int = 1280
output_height: int = 720
cover_url: str = ""
custom_title: str = ""
logs: str = "[]"
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@@ -142,6 +147,11 @@ class GenerationTask:
auto_retry_enabled: bool = False,
auto_retry_max: int = 0,
is_preview: bool = False,
source_task_id: str = "",
output_width: int = 1280,
output_height: int = 720,
cover_url: str = "",
custom_title: str = "",
) -> "GenerationTask":
if not project_id.strip() and not template_id.strip():
raise ValueError("project_id 或 template_id 至少需要提供一个")
@@ -167,6 +177,11 @@ class GenerationTask:
auto_retry_enabled=auto_retry_enabled,
auto_retry_max=auto_retry_max,
is_preview=is_preview,
source_task_id=source_task_id,
output_width=output_width,
output_height=output_height,
cover_url=cover_url,
custom_title=custom_title,
)
# ── 状态查询 ────────────────────────────────────────────────────────────
+77 -12
View File
@@ -11,6 +11,7 @@
from __future__ import annotations
import random
from typing import List
from packages.domain.edit_plan_clip import EditPlanClip
@@ -30,6 +31,9 @@ def distribute_assets(
clips: List[EditPlanClip],
asset_ids: List[str],
editing_mode: str,
*,
random_selection: bool = False,
asset_durations: dict[str, float] | None = None,
) -> None:
"""按 editing_mode 将素材分配到 clips(就地修改).
@@ -43,66 +47,85 @@ def distribute_assets(
clips: 剪辑片段列表(就地修改 asset_id)
asset_ids: 素材 ID 列表
editing_mode: 剪辑模式字符串
random_selection: 是否随机选择素材(用于预览生成)
asset_durations: 素材 ID -> 时长(秒)映射,用于设置随机 start_time
"""
if not asset_ids or not clips:
return
# 如果需要随机选择,先打乱素材顺序
if random_selection:
asset_ids = list(asset_ids) # 复制避免修改原列表
random.shuffle(asset_ids)
if editing_mode == EditingMode.ONE_TAKE.value:
_distribute_one_take(clips, asset_ids)
_distribute_one_take(clips, asset_ids, asset_durations)
elif editing_mode == EditingMode.PIP.value:
_distribute_pip(clips, asset_ids)
_distribute_pip(clips, asset_ids, asset_durations)
elif editing_mode == EditingMode.VOICE_OVER.value:
_distribute_voice_over(clips, asset_ids)
_distribute_voice_over(clips, asset_ids, asset_durations)
elif editing_mode == EditingMode.VOICE_PIP.value:
_distribute_voice_pip(clips, asset_ids)
_distribute_voice_pip(clips, asset_ids, asset_durations)
else:
# 未知模式,退化为 one_take
_distribute_one_take(clips, asset_ids)
_distribute_one_take(clips, asset_ids, asset_durations)
def _distribute_one_take(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
) -> None:
"""ONE_TAKE: 素材按顺序依次分配给 main 类型 clips."""
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
for i, clip in enumerate(main_clips):
if i < len(asset_ids):
clip.assign_asset(asset_ids[i])
asset_id = asset_ids[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
clip.assign_asset(asset_id, start_time=start_time)
def _distribute_pip(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
) -> None:
"""PIP: 第1个素材→main(全屏背景),其余→overlay clips."""
# 第1个素材 → main clip
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
if main_clips and asset_ids:
main_clips[0].assign_asset(asset_ids[0])
asset_id = asset_ids[0]
start_time = _calc_random_start_time(asset_id, main_clips[0].duration, asset_durations)
main_clips[0].assign_asset(asset_id, start_time=start_time)
# 其余素材 → overlay clips
overlay_clips = [c for c in clips if c.clip_type == "overlay"]
remaining = asset_ids[1:]
for i, clip in enumerate(overlay_clips):
if i < len(remaining):
clip.assign_asset(remaining[i])
asset_id = remaining[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
clip.assign_asset(asset_id, start_time=start_time)
def _distribute_voice_over(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
) -> None:
"""VOICE_OVER: 素材→main clips (B-roll)."""
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
for i, clip in enumerate(main_clips):
if i < len(asset_ids):
clip.assign_asset(asset_ids[i])
asset_id = asset_ids[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
clip.assign_asset(asset_id, start_time=start_time)
def _distribute_voice_pip(
clips: List[EditPlanClip],
asset_ids: List[str],
asset_durations: dict[str, float] | None = None,
) -> None:
"""VOICE_PIP: 第1个→background, 第2个→corner_voice, 其余→b_roll."""
bg_clips = [c for c in clips if c.clip_type == "background"]
@@ -113,19 +136,61 @@ def _distribute_voice_pip(
# 第1个 → background
if idx < len(asset_ids) and bg_clips:
bg_clips[0].assign_asset(asset_ids[idx])
asset_id = asset_ids[idx]
start_time = _calc_random_start_time(asset_id, bg_clips[0].duration, asset_durations)
bg_clips[0].assign_asset(asset_id, start_time=start_time)
idx += 1
# 第2个 → corner_voice
if idx < len(asset_ids) and voice_clips:
voice_clips[0].assign_asset(asset_ids[idx])
asset_id = asset_ids[idx]
start_time = _calc_random_start_time(asset_id, voice_clips[0].duration, asset_durations)
voice_clips[0].assign_asset(asset_id, start_time=start_time)
idx += 1
# 剩余 → b_roll clips
remaining = asset_ids[idx:]
for i, clip in enumerate(broll_clips):
if i < len(remaining):
clip.assign_asset(remaining[i])
asset_id = remaining[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
clip.assign_asset(asset_id, start_time=start_time)
# ── 随机 start_time 计算 ────────────────────────────────────────────────────
def _calc_random_start_time(
asset_id: str,
clip_duration: float,
asset_durations: dict[str, float] | None,
) -> float | None:
"""计算随机 start_time.
在素材总时长范围内随机取点,确保 clip_duration 不超出素材边界。
如果 asset_durations 为 None 或素材不在其中,返回 None(使用默认 0.0)。
Args:
asset_id: 素材 ID
clip_duration: 片段时长(秒)
asset_durations: 素材 ID -> 时长映射
Returns:
随机 start_time 或 None
"""
if asset_durations is None:
return None
total_duration = asset_durations.get(asset_id)
if total_duration is None or total_duration <= 0:
return None
# 最大起始点 = 素材总时长 - 片段时长
max_start = max(0.0, total_duration - clip_duration)
if max_start <= 0:
return 0.0
return random.uniform(0.0, max_start)
# ── clip_type 映射 ────────────────────────────────────────────────────────
+4 -1
View File
@@ -1,9 +1,10 @@
"""Quota system with registry pattern.
Three subscription tiers with different limits:
Four subscription tiers with different limits:
- free: 2GB storage, 5 videos/month, 3 concurrent, 3 templates, 50 titles, 10 voiceovers, no AI voice
- basic: 20GB storage, 30 videos/month, 10 concurrent, 15 templates, 500 titles, 100 voiceovers, AI voice
- premium: 100GB storage, 100 videos/month, 20 concurrent, unlimited templates, 500 titles, 100 voiceovers, AI voice
- pro: Same as premium (alias for premium tier)
Quota dimensions are registered by modules via the ModuleRegistry,
and checked against the user's subscription plan.
@@ -100,6 +101,8 @@ QUOTA_TIERS: Dict[str, QuotaTier] = {
},
),
}
# pro 套餐与 premium 配额相同,使用别名引用避免重复维护
QUOTA_TIERS["pro"] = QUOTA_TIERS["premium"]
class QuotaWarningLevel:
+207
View File
@@ -0,0 +1,207 @@
"""统一智能选素材算法 — 合并 _helpers / generation_tasks / auto_clip_service 的重叠逻辑。
设计目标:
- 单一入口,替代 3 套分散的选素材代码
- 多维度加权评分:质量分 + 时长均衡 + 新鲜度 + 未使用偏好
- 多样性保障:按时长分桶(短/中/长)均衡选取,避免同质化
- 可扩展:后续接入 AI 模型时只需替换 score_asset()
"""
from __future__ import annotations
import math
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
@dataclass
class SmartMatchResult:
"""单条素材的匹配结果。"""
asset: Any # Asset entity
score: float # 综合得分 0-100
breakdown: dict[str, float] = field(default_factory=dict) # 各维度得分明细
def _get_enum_value(obj: Any, attr: str) -> str:
"""安全获取属性值,兼容 StrEnum / 普通字符串。"""
val = getattr(obj, attr, None)
if val is None:
return ""
return val.value if hasattr(val, "value") else str(val)
def _duration_bucket(duration: float | None) -> str:
"""将素材时长分为 3 档:short(<10s) / medium(10-30s) / long(>30s)。"""
if duration is None or duration <= 0:
return "unknown"
if duration < 10:
return "short"
if duration <= 30:
return "medium"
return "long"
def score_asset(
asset: Any,
now: datetime | None = None,
) -> tuple[float, dict[str, float]]:
"""为单个素材计算综合得分(0-100)。
维度权重:
- quality_score (40%):素材质量分(0-100),无质量分按 50 计
- duration_fitness (30%):时长适配度,5-30s 为最优区间
- recency (20%):新鲜度,30 天内衰减
- unused_bonus (10%):未被使用过的素材加分
Returns:
(total_score, breakdown_dict)
"""
if now is None:
now = datetime.now(timezone.utc)
breakdown: dict[str, float] = {}
# 1. 质量分 (0-100) → 权重 40%
raw_quality = asset.quality_score if asset.quality_score is not None else 50.0
quality_component = raw_quality * 0.4
breakdown["quality"] = round(quality_component, 2)
# 2. 时长适配度 (0-100) → 权重 30%
# 最优区间 5-30s 得满分,越偏离越低
duration = getattr(asset, "duration", None) or 0.0
if duration <= 0:
duration_fitness = 30.0 # 未知时长给中等分
elif 5 <= duration <= 30:
duration_fitness = 100.0
elif duration < 5:
# 0-5s: 线性增长 20→100
duration_fitness = 20.0 + (duration / 5) * 80
else:
# >30s: 指数衰减,60s 时约 50 分
duration_fitness = 100.0 * math.exp(-0.02 * (duration - 30))
duration_fitness = max(duration_fitness, 10.0)
duration_component = duration_fitness * 0.3
breakdown["duration"] = round(duration_component, 2)
# 3. 新鲜度 (0-100) → 权重 20%
# 30 天半衰期
created_at = getattr(asset, "created_at", None)
if created_at is None:
recency = 50.0
else:
if created_at.tzinfo is None:
created_at = created_at.replace(tzinfo=timezone.utc)
age_days = max(0, (now - created_at).total_seconds() / 86400)
recency = 100.0 * math.exp(-0.05 * age_days) # ~14天半衰期
recency_component = recency * 0.2
breakdown["recency"] = round(recency_component, 2)
# 4. 未使用偏好 (0-100) → 权重 10%
metadata = getattr(asset, "metadata", None) or {}
try:
use_count = int(metadata.get("generation_use_count") or 0)
except (ValueError, TypeError):
use_count = 0 # 脏数据时按未使用处理(保守策略:给未使用加分)
if use_count == 0:
unused_score = 100.0
elif use_count <= 3:
unused_score = 70.0
else:
unused_score = 30.0
unused_component = unused_score * 0.1
breakdown["unused"] = round(unused_component, 2)
total = quality_component + duration_component + recency_component + unused_component
return round(total, 2), breakdown
def smart_select_assets(
assets: list[Any],
*,
limit: int | None = None,
kind: str | None = None,
now: datetime | None = None,
) -> list[SmartMatchResult]:
"""从素材列表中智能选取素材。
Args:
assets: 候选素材列表(Asset 实体)
limit: 最大返回数量,None 表示不限制
kind: 按文件类型过滤(video/image/audio),None 表示不过滤
now: 当前时间(用于测试注入)
Returns:
按得分降序排列的 SmartMatchResult 列表
"""
# Step 1: 过滤 ready 状态
ready_assets = [a for a in assets if _get_enum_value(a, "status") == "ready"]
# Step 2: 按 kind 过滤
if kind:
ready_assets = [a for a in ready_assets if a.file_type == kind]
if not ready_assets:
return []
# Step 3: 评分
scored: list[SmartMatchResult] = []
for a in ready_assets:
total, breakdown = score_asset(a, now=now)
scored.append(SmartMatchResult(asset=a, score=total, breakdown=breakdown))
# Step 4: 按得分降序排序
scored.sort(key=lambda r: r.score, reverse=True)
# Step 5: 多样性保障 — 时长分桶均衡选取
if limit and limit > 0 and len(scored) > limit:
scored = _diversity_select(scored, limit)
elif limit and limit > 0:
scored = scored[:limit]
return scored
def _diversity_select(scored: list[SmartMatchResult], limit: int) -> list[SmartMatchResult]:
"""从已排序的候选中按分桶均衡选取,避免全选中同一时长档。
策略:轮流从 short/medium/long 桶中按得分顺序取,直到凑满 limit。
"""
buckets: dict[str, list[SmartMatchResult]] = {
"short": [],
"medium": [],
"long": [],
"unknown": [],
}
for r in scored:
bucket = _duration_bucket(getattr(r.asset, "duration", None))
buckets.setdefault(bucket, []).append(r)
selected: list[SmartMatchResult] = []
selected_ids: set[str] = set()
bucket_order = ["medium", "short", "long", "unknown"] # medium 优先
bucket_idx = {b: 0 for b in bucket_order}
while len(selected) < limit:
added = False
for b in bucket_order:
if len(selected) >= limit:
break
items = buckets.get(b, [])
idx = bucket_idx[b]
while idx < len(items):
candidate = items[idx]
idx += 1
if candidate.asset.id not in selected_ids:
selected.append(candidate)
selected_ids.add(candidate.asset.id)
added = True
break
bucket_idx[b] = idx
if not added:
break
# 按原始得分降序输出
selected.sort(key=lambda r: r.score, reverse=True)
return selected
+2 -1
View File
@@ -332,7 +332,8 @@ def main():
print(f" {line}")
# 提交修复
run("git add -A")
run("git clean -fd")
run("git add -u")
run('git commit -m "style: auto-format with black + isort + prettier [skip ci-format-check]"')
# 推送(head_branch已从ensure_git_repo获取)
+1 -1
View File
@@ -75,7 +75,7 @@ def check_required_contexts(token, repo, sha, contexts):
for ctx in contexts:
state = statuses.get(ctx, "pending")
if state != "success":
if state not in ("success", "skipped"):
all_success = False
if state == "pending":
any_pending = True
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# scripts/ci/run_staging_tests.sh
# 在 staging 环境中运行 Playwright 测试
# 用法: bash scripts/ci/run_staging_tests.sh <mode>
# mode: e2e | api
#
# 解决 .gitea/workflows/ci-pipeline.yml 中多层引号嵌套问题:
# - 外层 YAML → bash → docker create → 容器内 sh/bash → 字符串解析
# - 提取为脚本后,只有两层:bash → 容器内 bash(单引号保护)
set -eu
MODE="${1:-e2e}"
CONTAINER_NAME="staging-${MODE}-$$"
# 强制清理可能残留的同名容器
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
if [ "$MODE" = "e2e" ]; then
docker create --name "$CONTAINER_NAME" --ipc=host \
-e E2E_BASE_URL=https://staging.xiaoxiajianji.com \
-e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \
-e E2E_BROWSER_CHANNEL=chromium \
-e PLAYWRIGHT_HEADLESS=1 \
-w /workspace/apps/web \
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
bash -c 'for i in 1 2 3; do npm ci --registry=https://registry.npmmirror.com && break; echo "npm ci attempt $i failed, retrying in 15s..."; sleep 15; done && npx playwright test --reporter=line --project=chromium e2e/auth.spec.ts e2e/auth-guard.spec.ts e2e/core-upload.spec.ts e2e/core-generation.spec.ts'
elif [ "$MODE" = "api" ]; then
docker create --name "$CONTAINER_NAME" \
-e E2E_BASE_URL=https://staging.xiaoxiajianji.com \
-e E2E_API_BASE=https://staging-api.xiaoxiajianji.com/api/v1 \
-w /workspace/apps/web \
git.xiaoxiajianji.com/xiaoxia/base/playwright:v1.45.0-jammy \
bash -c 'for i in 1 2 3; do npm ci --registry=https://registry.npmmirror.com && break; echo "npm ci attempt $i failed, retrying in 15s..."; sleep 15; done && npx playwright test --reporter=line e2e/test_auth.spec.ts e2e/test_asset.spec.ts e2e/test_project.spec.ts'
else
echo "ERROR: Unknown mode '$MODE'. Use 'e2e' or 'api'." >&2
exit 1
fi
# 把代码拷进容器
docker cp apps "$CONTAINER_NAME:/workspace/"
# 启动并等待
docker start -a "$CONTAINER_NAME"
EXIT_CODE=$(docker wait "$CONTAINER_NAME")
# 清理容器
docker rm "$CONTAINER_NAME" 2>/dev/null || true
exit "$EXIT_CODE"
+1 -1
View File
@@ -100,7 +100,7 @@ else
-m pytest tests/unit -q
python3 -m coverage report --show-missing
python3 -m coverage xml -o coverage.xml
python3 -m coverage report --fail-under=65 > /dev/null
python3 -m coverage report --fail-under=55 > /dev/null
fi
# --- Diff 覆盖率检查(仅PR ---
+2 -5
View File
@@ -9,10 +9,7 @@ echo "=== 前端依赖安装开始 (模式: $MODE) ==="
cd apps/web
# 配置国内镜像加速
npm config set registry https://registry.npmmirror.com
# 安装依赖
npm ci --no-audit --no-fund
# 安装依赖(使用国内镜像加速
npm ci --registry=https://registry.npmmirror.com --no-audit --no-fund
echo "=== 前端依赖安装完成 ==="
+15 -17
View File
@@ -1,7 +1,8 @@
"""模式渲染集成测试.
"""剪辑模式渲染集成测试.
验证 4 剪辑模式ONE_TAKE / PIP / VOICE_OVER / VOICE_PIP通过
验证剪辑模式ONE_TAKE / VOICE_OVER通过
_build_plan_and_clips_from_task + UnifiedRenderService 的完整渲染流程
PIP / VOICE_PIP 已下线统一映射为 ONE_TAKE
需要 ffmpeg 可用CI ffmpeg 时自动跳过
"""
@@ -109,14 +110,13 @@ class TestBuildPlanAndClips:
assert all(c.clip_type == "main" for c in clips)
assert len(asset_map) == 3
def test_pip_mode(self):
def test_pip_mode_maps_to_one_take(self):
"""PIP 已下线,映射为 one_take → 全部 main clips。"""
paths = self._make_paths(3)
plan, clips, asset_map = _build_plan_and_clips_from_task("t2", paths, "pip")
assert len(clips) == 3
assert clips[0].clip_type == "main"
assert clips[1].clip_type == "overlay"
assert clips[2].clip_type == "overlay"
assert all(c.clip_type == "main" for c in clips)
def test_voice_over_mode(self):
paths = self._make_paths(3)
@@ -126,15 +126,13 @@ class TestBuildPlanAndClips:
assert all(c.clip_type == "main" for c in clips)
assert all(c.config.get("role") == "b_roll" for c in clips)
def test_voice_pip_mode(self):
def test_voice_pip_mode_maps_to_one_take(self):
"""VOICE_PIP 已下线,映射为 one_take → 全部 main clips。"""
paths = self._make_paths(4)
plan, clips, asset_map = _build_plan_and_clips_from_task("t4", paths, "voice_pip")
assert len(clips) == 4
assert clips[0].clip_type == "background"
assert clips[1].clip_type == "corner_voice"
assert clips[2].clip_type == "b_roll"
assert clips[3].clip_type == "b_roll"
assert all(c.clip_type == "main" for c in clips)
def test_unknown_mode_defaults_to_one_take(self):
paths = self._make_paths(2)
@@ -166,13 +164,13 @@ class TestFourModeLayerGrouping:
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"main"}
def test_pip_layers(self):
"""PIP: 1 main + 2 overlay → main + overlay。"""
def test_pip_layers_now_one_take(self):
"""PIP 已下线 → one_take: 3 main → 1 main layer"""
paths = [Path(f"/tmp/pip_{i}.mp4") for i in range(3)]
_, clips, _ = _build_plan_and_clips_from_task("pip", paths, "pip")
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"main", "overlay"}
assert roles == {"main"}
def test_voice_over_layers(self):
"""VOICE_OVER: 3 main(b_roll) → broll。"""
@@ -182,13 +180,13 @@ class TestFourModeLayerGrouping:
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"broll"}
def test_voice_pip_layers(self):
"""VOICE_PIP: 1 bg + 1 corner_voice + 2 b_roll → 3 个图层"""
def test_voice_pip_layers_now_one_take(self):
"""VOICE_PIP 已下线 → one_take: 4 main → main layer"""
paths = [Path(f"/tmp/vpip_{i}.mp4") for i in range(4)]
_, clips, _ = _build_plan_and_clips_from_task("vpip", paths, "voice_pip")
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"background", "corner_voice", "broll"}
assert roles == {"main"}
# ── 端到端渲染测试(需要 ffmpeg) ─────────────────────────────────────────────

Some files were not shown because too many files have changed in this diff Show More