Compare commits

..

143 Commits

Author SHA1 Message Date
xiaoxia 3441b3fece fix: Canvas 播放黑屏二次修复 — FrameQueue 扩容到 600 + 按需解码限制 300 帧
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 / Build Staging Worker Image (push) Successful in 1m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 51s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m6s
CI/CD Pipeline / Integration Tests (push) Successful in 4m6s
CI/CD Pipeline / Unit Tests (push) Successful in 13m54s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
根因:按需解码 decodeAroundPosition 调用 decodeSegment 时未传 maxFrames,
导致一次解码整个片段(~300帧)全部 push 进 maxSize=60 的队列,前 240 帧被淘汰,
只剩末尾帧(pts≈8~10s),getCurrentFrame(0) 找不到帧 → 黑屏 3~4 秒。

修复:
- FrameQueue maxSize: 60 → 600(覆盖约 20 秒 @30fps)
- decodeAroundPosition 中 decodeSegment 传入 maxFrames=300,
  限制单次按需解码帧数,避免远超队列容量的帧挤兑
2026-08-21 00:46:41 +08:00
xiaoxia b7be479ae7 fix: staging test_asset.spec.ts 适配 POST /assets 返回 410 (#1453)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 57s
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 2m56s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m3s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 53s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m15s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m3s
CI/CD Pipeline / Unit Tests (push) Successful in 13m55s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m59s
CI/CD Pipeline / CI Gate (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-21 00:06:01 +08:00
xiaoxia 95c33f6604 fix: Canvas 内部分辨率不匹配导致画面变形 — 修复 ResizeObserver 未创建问题
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 / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m18s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m5s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m18s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 3m42s
CI/CD Pipeline / Unit Tests (push) Successful in 14m8s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 6m3s
CI/CD Pipeline / CI Gate (push) Has been skipped
根因:ResizeObserver useEffect 依赖数组为空 [],组件挂载时 canPlay=false
导致 canvas 未渲染、canvasRef 为 null,effect 直接 return 后永远不会重新执行,
canvas 内部分辨率停留在浏览器默认值 300×150(横屏 2:1),而 CSS 显示为竖屏容器。

修复:
- useEffect 依赖改为 [useWebCodecs, canPlay],canPlay=true 时 canvas 已渲染,
  effect 重新执行并正确创建 ResizeObserver
- 创建 ResizeObserver 前立即通过 getBoundingClientRect 设置 canvas 初始像素分辨率,
  避免首帧使用默认 300×150 导致变形
- computeDrawRect 已正确基于 videoDimRef 做 letterbox 居中绘制,无需修改
2026-08-20 23:49:57 +08:00
xiaoxia 161c1a61b6 fix: HEVC 转码条件解耦 + 缩略图函数补全 + extra_meta 字段 (#1452)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m11s
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 2m24s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 52s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m22s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 3m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m25s
CI/CD Pipeline / Unit Tests (push) Successful in 12m58s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m59s
CI/CD Pipeline / CI Gate (push) Has been skipped
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
AI Code Review / AI Code Review (pull_request) Successful in 38s
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 / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
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 / Validate - Migration (alembic) (pull_request) Successful in 3m7s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m35s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m39s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m4s
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 / Integration Tests (pull_request) Successful in 4m56s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-20 21:41:01 +08:00
xiaoxia 8b30193f3e fix: Canvas 预览黑屏修复 - 增大帧缓冲 + 限制初始化预解码 + play 同步清空帧队列
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
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 2m47s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m53s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m21s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m17s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 3m12s
CI/CD Pipeline / Unit Tests (push) Failing after 12m58s
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 / 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
CI/CD Pipeline / Integration Tests (push) Failing after 5m37s
CI/CD Pipeline / CI Gate (push) Has been skipped
- FrameQueue maxSize 从 10 改为 60(约 2 秒 @30fps),给按需解码留出足够预缓冲空间
- 新增 MAX_INIT_FRAMES=60 常量,decodeSegment 支持 maxFrames 参数,初始化阶段只解码前 60 帧避免帧挤兑
- play() 重播时同步清空 frameQueue(之前只清 decodedSegments 标记没清帧缓冲)
- play() 启动渲染循环后立即触发 decodeAroundPosition,不等 200ms 节流延迟
2026-08-20 19:21:32 +08:00
xiaoxia 665228a58f refactor: 删除 uploadAsset,统一使用 uploadAssetDirect 直传
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 / Build Staging Worker Image (push) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m49s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m36s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m53s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 3m38s
CI/CD Pipeline / Unit Tests (push) Failing after 14m29s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 7m28s
CI/CD Pipeline / CI Gate (push) Has been skipped
前端:
- 删除 uploadAsset 函数(upload.ts)和 export(index.ts)
- 删除 UploadResult type export
- CloneModal/useCloneSubmit 改用 uploadAssetDirect
- DirectUploadCompleteResult 新增 url 字段
- 清理测试文件中 uploadAsset 引用

后端:
- DirectUploadCompleteResponse 新增 url 字段
- complete_direct_upload 返回 OSS 文件 URL
- 修复 FFmpeg 超时日志 300s → 900s
2026-08-20 16:22:56 +08:00
CI Bot 56d45c2d49 fix: 清理 create_asset 废弃后的 unused imports
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m0s
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 2m50s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m3s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m24s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
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 / 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
- 删除 CreateAssetRequest import
- 删除 CreateAssetCommand/UseCase imports
- 删除 AssetStatus/ClassificationStatus imports
- 修复 ingest.py 日志信息 (300s → 900s)
2026-08-20 16:17:23 +08:00
CI Bot ab4a1b7262 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 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 Worker Image (push) Successful in 43s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m45s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m0s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 5m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m20s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 3m6s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / 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 / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-20 08:07:14 +00:00
xiaoxia a39e5ea936 refactor: 清理前端 createAsset 死代码和未使用 import
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 / Build Staging API Image (push) Successful in 56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 57s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m21s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m40s
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 / 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
- 删除 test/api/assets.test.ts 中 createAsset 的 import 和测试块
- 删除 GeneratePage.test.tsx 和 VoiceMaterialLibrary.test.tsx 中 createAsset mock
- 清理 voices/types.ts 和 voice-materials/types.ts 中 createAsset 注释
- 删除 useVoiceUpload.ts 中未使用的 IngestJob type import
2026-08-20 16:02:05 +08:00
CI Bot 2d0d8a2777 chore: 清理上传路径统一后的残留死代码
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 / Build Staging Worker Image (push) Successful in 54s
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 / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web 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
后端:
- create_asset 函数简化为仅返回 410,删除 46 行死代码
- 移除无用的依赖注入参数(asset_repository 等)

前端:
- 删除 createAsset 函数定义(assets.ts)
- 删除 createAsset 的 re-export(index.ts)

注: uploadAsset 函数保留(语音克隆 CloneModal 仍在使用)
2026-08-20 15:59:44 +08:00
CI Bot d3469d9799 refactor: 统一所有素材上传为单一 ingest 路径
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 / Build Staging Worker Image (push) Successful in 1m0s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m22s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m28s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m29s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m18s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 5m10s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 5m40s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (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
前端修改:
1. useVoiceUpload.ts (voices): 删除 createAsset 调用,改为轮询 ingest_job_id
2. useVoiceUpload.ts (voice-materials): 同上,使用 result_asset_id 打标签

后端修改:
1. POST /assets 返回 410 Gone(已废弃,统一走 ingest-jobs)
2. 上传流程: uploadAssetDirect → completeDirectUpload → _submit_ingest_job → Worker

统一后的上传路径:
- 前端上传 OSS(直传)
- 调用 completeDirectUpload 创建 ingest job
- Worker 根据类型处理(视频转码/图片音频元数据提取)
- 创建 Asset 记录 → ready
- 前端轮询 job 状态,完成后刷新
2026-08-20 15:54:01 +08:00
CI Bot 068010b059 feat: 统一素材上传为单一 ingest 路径
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 / Build Staging Worker Image (push) Successful in 53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m14s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
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 / 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
后端改动:
1. POST /assets 拒绝视频类型(必须走 ingest-jobs)
2. HEVC 转码超时从 300s 改为 900s(支持大文件)
3. 添加 HDR→SDR 转换参数(colorspace/primaries/transfer=bt709)
4. 转码前检查磁盘空间(< 2GB 时拒绝任务)

前端无需修改:
- 当前 uploadAssetDirect 已统一走 ingest-jobs 流程
- completeDirectUpload → _submit_ingest_job → Worker 处理
- 视频上传后自动进入转码+入库流程
2026-08-20 15:48:17 +08:00
xiaoxia 231d5153bf fix: CSS 残留 6 处 aspect-ratio 16/9 改为 9/16
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 / Build Staging Worker Image (push) Successful in 1m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m28s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m50s
CI/CD Pipeline / Unit Tests (push) Successful in 16m56s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
统一竖屏 9:16 比例,清理 generate.css 中所有残留的横屏比例:
- .xx-preview-video (894)
- .xx-smart-match-thumb (1519)
- .xx-cover-frame-placeholder (2147)
- .xx-cover-upload-area (2250)
- .xx-cover-preview-box (2311)
- .xx-preview-video-wrapper .xx-preview-video (2669)
2026-08-20 15:30:29 +08:00
xiaoxia f4af6ec8b7 fix: 删除用 mp4box.js 维度数据设置 videoDimRef 的代码
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 54s
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 2m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m44s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m50s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 49s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m9s
CI/CD Pipeline / Unit Tests (push) Successful in 17m7s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 36m33s
CI/CD Pipeline / CI Gate (push) Has been skipped
- 删除 decoder configure 成功后的 videoDimRef 设置
- 删除 init 函数中的 videoDimRef 设置
- 唯一可靠的尺寸来源改为 VideoDecoder output 回调从 VideoFrame 获取的真实帧尺寸
2026-08-20 14:37:07 +08:00
xiaoxia 6a3f4e4127 fix: 统一竖屏 9:16 比例
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 / Build Staging Worker Image (push) Successful in 58s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m17s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m59s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 48s
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 / 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 / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
- generate.css: 7 处 aspect-ratio 从 16/9 改为 9/16
- generate.css: .xx-preview-video max-height 400px→600px
- generate.css: .xx-preview-video-wrapper max-width→max-height
- PreviewVideoPanel.tsx: fallback 从 16:9→9:16
- UploadCoverPicker.tsx: 提示文字从 建议16:9→建议9:16
2026-08-20 14:29:05 +08:00
CI Bot ec3ba19342 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 / Build Staging Worker Image (push) Successful in 58s
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 2m39s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m40s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m22s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m31s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m47s
CI/CD Pipeline / Unit Tests (push) Successful in 12m6s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-20 06:15:28 +00:00
CI Bot 26728dfffb fix: HEVC 转码支持竖屏视频自动旋转
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 / Build Staging Worker Image (push) Successful in 1m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m2s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m34s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m17s
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 / 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
- 转码前用 ffprobe 检测 rotation 元数据
- rotation=90/270 时添加 transpose=1 物理旋转画面
- 添加 -metadata:s:v:0 rotate=0 清除旋转元数据
- 添加 -level 4.0 确保 H.264 level 支持 1080p
- scale 滤镜在旋转之后应用

修复前端 VideoDecoder 解码竖屏 HEVC 视频报错的问题
2026-08-20 14:11:34 +08:00
xiaoxia cb903e3bd6 fix: 移除 rotation 元数据检测,从 VideoFrame 获取实际尺寸
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 - 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 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
- 删除 extractVideoRotation 函数
- 删除 demuxSegment 和 decodeSegment 中的旋转检测+宽高交换代码
- 删除 decoder.configure() 中的 codedWidth/codedHeight 参数
- 在 VideoDecoder output 回调中从第一帧获取实际帧尺寸
- 核心思路:不再依赖 mp4box.js 的 track_width/track_height 和 rotation 元数据,
  改为从 VideoDecoder 实际输出的 VideoFrame 获取真实尺寸
2026-08-20 14:11:13 +08:00
xiaoxia b447ad84ea fix: vitest 全局超时从 5s 提到 15s,防止 CI 高负载偶发超时 (#1451)
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 2m43s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m39s
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 Web 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-20 14:06:00 +08:00
xiaoxia 2356f12f11 fix: 修复 VideoDecoder flush timeout 解码器超时卡死
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m0s
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 2m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m11s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 4m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m24s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Unit Tests (push) Successful in 11m8s
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 / 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 / Integration Tests (push) Successful in 4m31s
CI/CD Pipeline / CI Gate (push) Has been skipped
- decoder.decode(chunk) 改为 await,捕获异步 rejection
- 添加 decodeErrors 计数,连续 3 次解码失败跳过当前片段
- flush 之前检查 decoder.state,只在 configured 状态才执行
- 避免在 error/closed 状态调用 flush 导致卡死
2026-08-20 13:31:57 +08:00
xiaoxia 3c42541ff7 fix: 解析 MP4 rotation 元数据,修复竖屏视频显示为横屏
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 / Build Staging Worker Image (push) Successful in 1m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m58s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m16s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 50s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m57s
CI/CD Pipeline / Unit Tests (push) Successful in 11m58s
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 API 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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
- 新增 extractVideoRotation 函数,从 tkhd box 解析旋转角度
- demuxSegment 中检测 90°/270° 旋转,交换宽高
- decodeSegment 中同样检测旋转,确保 configure 使用正确尺寸
- 修复 iPhone 竖屏视频在竖屏容器中显示为一小块横屏画面的问题
2026-08-20 13:08:24 +08:00
xiaoxia 85afadd166 feat: 上传时自动检测 HEVC 编码并转码为 1080p H.264 (#1449)
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 / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m44s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m13s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m56s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m33s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 2m42s
CI/CD Pipeline / Unit Tests (push) Successful in 11m28s
CI/CD Pipeline / Build Production API 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 / 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-20 12:10:15 +08:00
xiaoxia 634d05c010 fix: 修复 AI Code Review 阻塞级问题 v3(useCanvasPlayer 解码策略+资源泄漏+useStep6Cover 轮询超时) (#1450)
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 / Build Staging Worker Image (push) Successful in 1m30s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m54s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m40s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m39s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration 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 / 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-20 12:01:01 +08:00
张宏杰 355431b867 feat: decodeSegment 增加解码进度日志和 flush 超时保护
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 / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m6s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 44s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m20s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Unit Tests (push) Successful in 11m38s
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 API 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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
- 记录实际 decode 的 sample 数量、跳过数量、decoder 状态
- flush 增加 10 秒超时保护,防止无限卡住
- configure 成功后立即清除 isBuffering 状态,让 UI 不再显示'准备中'
2026-08-19 23:58:23 +08:00
张宏杰 041cec8670 fix: findCodecConfig 重写,正确处理 stsd 和容器 box 递归
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 36s
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 1m57s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m25s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m29s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Successful in 11m10s
CI/CD Pipeline / Build Production API 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 / 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
- 新增 MP4_CONTAINER_TYPES 常量,包含 trak/mdia/minf/stbl 等标准容器
- stsd 特殊处理:额外 8 字节头(version/flags + entry_count),从 offset+16 开始
- VisualSampleEntry 保持 offset+8+78(78 字节固定字段)
- 删除 fallback 双重搜索逻辑,改为明确的容器类型判断
2026-08-19 23:24:00 +08:00
张宏杰 472f371d2f fix: VisualSampleEntry 添加 fallback 搜索,兼容不同 MP4 结构
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 / Build Staging Worker Image (push) Successful in 56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m36s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m49s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m28s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 46s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m1s
CI/CD Pipeline / Unit Tests (push) Successful in 11m42s
CI/CD Pipeline / Integration Tests (push) Successful in 4m12s
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 / 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
- 先尝试 offset+8+78(标准 VisualSampleEntry 结构)
- 未找到则 fallback 到 offset+8(兼容非标准 MP4)
- 添加调试日志追踪搜索过程
2026-08-19 22:51:25 +08:00
张宏杰 709d65ce52 fix: VisualSampleEntry 跳过78字节固定字段,修复 hvcC 提取位置
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m21s
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 2m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m25s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m16s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Unit Tests (push) Successful in 11m9s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m16s
CI/CD Pipeline / CI Gate (push) Has been skipped
- findCodecConfigRecursive 区分 VisualSampleEntry 和普通容器 box
- avc1/avc3/hvc1/hev1 前 78 字节是固定字段(ISO 14496-12),子 box 从 offset+8+78 开始
- 普通容器 box(moov/trak/mdia/minf/stbl)仍从 offset+8 开始递归
- 修复 HEVC description 提取失败导致的黑屏问题
2026-08-19 22:38:01 +08:00
张宏杰 ae1b9f8ddb fix: findCodecConfig 改为纯递归搜索,修复 HEVC description 提取失败
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 47s
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 2m22s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m40s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m55s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 44s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 48s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m20s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Integration Tests (push) Successful in 3m17s
CI/CD Pipeline / Unit Tests (push) Successful in 12m10s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
根因:旧 findCodecConfig 对 container box 的 fullbox 检测逻辑错误,
导致递归搜索时跳过了子 box 的起始位置,永远找不到 hvcC/avcC。

修复:
- 替换为 findCodecConfigRecursive,纯递归搜索所有嵌套 box
- 不区分 fullbox/非 fullbox,统一从 offset+8 开始搜索子 box
- 返回 hvcC/avcC 的内容(不含 8 字节 box header),符合 WebCodecs 要求
- 添加日志追踪找到的 box 类型和位置
2026-08-19 22:18:26 +08:00
张宏杰 1c9903574a fix: 修复预览播放器初始化流程断裂 + 确认生成改为直接创建任务
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 59s
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 2m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m5s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m24s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 11m28s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m11s
CI/CD Pipeline / CI Gate (push) Has been skipped
修复一:useCanvasPlayer 初始化 useEffect 中,loadSegment 内的 setState
触发重渲染 → cleanup 调用 destroy() → isDestroyedRef=true → demuxSegment
永远不执行。改用局部 cancelled 变量替代 isDestroyedRef 做异步流程控制,
添加 6 处调试日志追踪完整初始化流程。

修复二:确认生成流程从 TODO 占位改为直接调用 createGenerationTask,
传入 template_id/asset_ids/output_width/output_height/cover_url/
custom_title/title_config/video_ratio 等完整参数。

修复三:扩展 CreateGenerationTaskRequest 类型,新增 output_width/
output_height/cover_url/custom_title/duration/video_ratio/title_config
可选字段,title_ids/voice_ids 改为可选。
2026-08-19 21:52:44 +08:00
张宏杰 cfedc06df0 refactor: 清理预览旧架构 — 删除 confirmGeneration 调用 + previewTaskId 链路
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 / Build Staging Worker Image (push) Successful in 1m5s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m17s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m54s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m58s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m14s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Unit Tests (push) Successful in 13m45s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m1s
CI/CD Pipeline / CI Gate (push) Has been skipped
- useGenerateVideo: 删除 confirmGeneration 导入和调用,改为 TODO 占位
- GeneratePage: 删除 previewTaskId 硬编码空字符串
- types: 删除 previewTaskId 字段定义
- api/generation: 删除 confirmGeneration 导出和 ConfirmGeneration 类型导出
- createPreview/getPreviewStatus 保留(useStep6Cover 封面流程仍在使用)
2026-08-19 20:45:18 +08:00
张宏杰 4429784a79 refactor: 删除 createPreview 调用,确认生成不再触发后端预览渲染
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 / Build Staging Worker Image (push) Successful in 44s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m22s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m39s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m26s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m52s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / 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 / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
- 删除 createPreview API 导入和调用
- 预览已改为前端实时播放(Canvas + WebCodecs)
- taskId 改为直接使用 props.previewTaskId
- 移除 useCallback 依赖中的 selectedTemplate
2026-08-19 20:35:07 +08:00
张宏杰 aa36e63591 refactor: 清理 descriptionCache 兜底代码
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 - 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 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
删除 4 处 descriptionCache 相关代码:
- 删除 useRef 声明
- 删除缓存兜底块
- 删除缓存写入
- 删除缓存清理

descriptionCache 不再需要,description 直接由 extractCodecDescription 提供
2026-08-19 20:33:16 +08:00
张宏杰 c3373de4f3 fix: findCodecConfig 动态检测 fullbox,修复 container box 偏移计算错误
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 / Build Staging Worker Image (push) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m21s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m25s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m8s
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 Web 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
问题根因:
- 代码假设所有 container box (trak/mdia/minf/stbl) 都是 fullbox
- 但实际 MP4 文件中这些 box 可能不是 fullbox,content 从 offset+8 开始而非 offset+12
- 导致递归查找时偏移错误,永远找不到 stsd → hvcC/avcC

修复方案:
- 动态检测 fullbox:检查 offset+8 处的 version/flags 字节是否为 0
- 如果是 fullbox,content 从 offset+12 开始;否则从 offset+8 开始
- 在 containerBoxes 列表中添加 'moov'
2026-08-19 20:29:46 +08:00
张宏杰 60d95e64ab fix: HEVC description偏移修正+调试日志 (#1450)
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 / Build Staging Worker Image (push) Successful in 1m0s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m57s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m41s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m51s
CI/CD Pipeline / Unit Tests (push) Successful in 14m48s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m41s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-19 20:06:44 +08:00
张宏杰 7775d5d118 fix: setExtractionOptions 改为 nbSamples: Infinity (#1449)
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 / Build Staging Worker Image (push) Successful in 58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m12s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m43s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m25s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Unit Tests (push) Successful in 11m36s
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 / 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
CI/CD Pipeline / Integration Tests (push) Successful in 5m49s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-19 19:53:35 +08:00
张宏杰 f025514d16 fix: Canvas播放器全量修复 - nbSamples/description/调试日志 (#1448)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m1s
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 2m20s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 57s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Integration Tests (push) Successful in 4m15s
CI/CD Pipeline / Unit Tests (push) Successful in 11m59s
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-19 19:27:09 +08:00
张宏杰 e043b0aa6a fix: demuxSegment添加5秒超时保护+防重复resolve (#1447)
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 / Build Staging Worker Image (push) Successful in 53s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m37s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 51s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration 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 / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-19 19:19:35 +08:00
张宏杰 fdce08356b fix: demuxSegment改为异步,等待onSamples完成后返回,修复黑屏根因 (#1446)
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 2m19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m56s
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 2m32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m15s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 49s
CI/CD Pipeline / Integration Tests (push) Successful in 2m46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 11m27s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
- demuxSegment 改为 async 函数,返回 Promise
- onSamples 完成后 resolve(meta),确保 samples 数据就绪
- init 中 await demuxSegment 避免空 samples 导致黑屏
2026-08-19 19:01:17 +08:00
张宏杰 89cccf294c fix: 全量可选链+空值兜底,杜绝 TypeError (#1445)
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 2m11s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m20s
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 Worker Image (push) Successful in 1m2s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 50s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 57s
CI/CD Pipeline / Integration Tests (push) Successful in 2m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 11m51s
CI/CD Pipeline / Build Production API 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 / Build Production Web 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-19 18:42:13 +08:00
张宏杰 5f3ff6bb8c feat: 前端自主视频切片,不再依赖后端切片 (#1444)
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 - Type Check (mypy) (push) Successful in 1m59s
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 2m18s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 53s
CI/CD Pipeline / Integration Tests (push) Successful in 2m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m6s
CI/CD Pipeline / Unit Tests (push) Successful in 11m36s
CI/CD Pipeline / Build Production API 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 / 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-19 18:26:55 +08:00
张宏杰 ca78b182af fix: Canvas description缓存+关键帧容错+data字段修正 (#1443)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m37s
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 Worker Image (push) Successful in 40s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m46s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m47s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 50s
CI/CD Pipeline / Integration Tests (push) Successful in 3m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m6s
CI/CD Pipeline / Unit Tests (push) Successful in 11m55s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-08-19 18:06:53 +08:00
张宏杰 ca8f1079b1 chore: retrigger CI (#1442)
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 / Build Staging Worker Image (push) Successful in 1m2s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m20s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m50s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 46s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
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 / 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 / Integration Tests (push) Successful in 4m7s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-19 17:46:08 +08:00
xiaoxia ee92f72252 fix: HEVC description返回完整box+标题分割调试日志 (#1442) (#1442)
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 - 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 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-19 17:46:00 +08:00
张宏杰 7b63219d9e fix: HEVC description返回完整box+标题分割调试日志 (#1442)
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 / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m6s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m11s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m11s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m23s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (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-19 17:37:04 +08:00
张宏杰 dfd821f5ba chore: retrigger CI
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m36s
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 / Build Staging Web Image (push) Successful in 2m8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m44s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m43s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m51s
CI/CD Pipeline / Unit Tests (push) Successful in 13m18s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m44s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-19 17:07:01 +08:00
xiaoxia bfc9e07170 fix: subBoxStart偏移修正+标题分割扩展Unicode斜杠字符 (#1441) (#1441)
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 / Build Staging Worker Image (push) Successful in 50s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m8s
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 / 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-19 17:01:06 +08:00
张宏杰 2c2fcabd6c fix: subBoxStart偏移修正+标题分割扩展Unicode斜杠字符 (#1441)
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 / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m2s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (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 / 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-19 16:58:52 +08:00
张宏杰 4e19d08a98 fix: MP4 fullbox偏移修正+标题全角/半角斜杠换行 (#1440)
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 / Build Staging Worker Image (push) Successful in 59s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m47s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m19s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Integration Tests (push) Successful in 3m19s
CI/CD Pipeline / Unit Tests (push) Successful in 11m45s
CI/CD Pipeline / Build Production API 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 / 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-19 14:45:17 +08:00
张宏杰 d9b4041215 fix: 预览播放器4个线上问题修复 (#1439)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
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 2m31s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Integration Tests (push) Successful in 3m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m26s
CI/CD Pipeline / Unit Tests (push) Successful in 11m50s
CI/CD Pipeline / Build Production API 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 / 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
- CSS弹窗裁剪: .ant-modal规则限定到.xx-preview-modal
- HEVC黑屏: 从MP4 buffer提取avcC/hvcC作为VideoDecoder description
- 标题换行: drawTitle按/分割多行绘制
- 封面400: useStep6Cover自动创建后端预览后重试
2026-08-19 14:10:47 +08:00
张宏杰 48d1b4dd33 fix: Canvas黑屏修复+配音时长校验fallback (#1438)
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 / Build Staging Worker Image (push) Successful in 1m4s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m46s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m53s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m55s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m6s
CI/CD Pipeline / Unit Tests (push) Successful in 10m49s
CI/CD Pipeline / Build Production API 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 / 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
- useCanvasPlayer renderFrame 删除 frame.close(),帧释放由 FrameQueue 统一管理
- 新增 calculateTotalVideoDuration 共享工具函数
- GeneratePage: 素材为空时用模板 duration_max 之和估算总时长
- Step5VoiceSelect: Number() 类型转换 + debug log
2026-08-19 13:16:44 +08:00
张宏杰 25ce6d6286 fix: Canvas播放器修复 - 片段自动切换+宽高比+移除死代码+进度节流 (#1437)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 57s
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 1m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m30s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 58s
CI/CD Pipeline / Integration Tests (push) Successful in 2m50s
CI/CD Pipeline / Unit Tests (push) Successful in 10m41s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
- 初始化时加载所有片段数据并逐段解码,帧时间戳按全局偏移对齐
- 新增computeDrawRect保持视频宽高比(letterbox/pillarbox)
- 移除未使用的audioCtxRef/audioSourceRef
- 进度更新节流到200ms(5fps)
- FrameQueue容量提升到10
- fallback路径优化预加载策略
2026-08-19 12:52:31 +08:00
xiaoxia 89a56eaf96 feat: Canvas + WebCodecs 预览播放器核心实现 (#1436)
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 / Build Staging Worker Image (push) Successful in 1m6s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m31s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m48s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m59s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m19s
CI/CD Pipeline / Integration Tests (push) Successful in 2m39s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m4s
CI/CD Pipeline / Unit Tests (push) Successful in 11m23s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Staging API Integration Tests (push) Successful in 3m32s
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-19 12:01:04 +08:00
张宏杰 71dddec3af feat: Canvas + WebCodecs 预览播放器核心实现 (#1436)
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 / Build Staging Worker Image (push) Successful in 53s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m27s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m40s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m7s
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 Web 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
- 新建 useCanvasPlayer.ts: mp4box解封装 + VideoDecoder解码 + FrameQueue环形缓冲区 + Canvas渲染
- 重写 FrontendPreviewPlayer.tsx: Canvas路径(主) + video元素fallback路径(兼容)
- 标题Canvas内合成(drawFillText),不再使用CSS overlay
- isWebCodecsSupported()运行时检测自动选择路径
- 安装mp4box依赖用于MP4解封装
2026-08-19 11:57:45 +08:00
xiaoxia 938b9d3529 fix: 视频预加载优化+配音时长校验提醒 (#1435) (#1435)
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 / Build Staging Worker Image (push) Successful in 52s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m54s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m30s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m34s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m47s
CI/CD Pipeline / Unit Tests (push) Successful in 11m30s
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 / 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 / Integration Tests (push) Successful in 4m3s
CI/CD Pipeline / CI Gate (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-19 11:36:02 +08:00
张宏杰 4102864218 fix: 视频预加载优化+配音时长校验提醒 (#1435)
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 / Build Staging Worker Image (push) Successful in 51s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m11s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m54s
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 Web 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
1. useSegmentScheduler: 挂载后强制调用 video.load() 预加载所有片段
2. useSegmentScheduler: 片段切换时先检查 readyState>=3 再 play(),否则监听 canplay + 300ms 超时兜底
3. FrontendPreviewPlayer: display:none/block 改为 opacity:0/1 + pointer-events,保持所有 video 在渲染树中
4. GeneratePage: 计算 totalVideoDuration 传给配音选择组件
5. Step5VoiceSelect: 配音时长不足时显示红色警告+Modal确认(仍要使用/重新选择)
6. 修复 ESLint react-hooks/exhaustive-deps 警告
2026-08-19 11:32:15 +08:00
张宏杰 95893916a2 chore: trigger CI retry - npm install transient failure
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m23s
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 2m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m34s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 49s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m23s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m37s
CI/CD Pipeline / Unit Tests (push) Successful in 11m10s
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 / 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 / Integration Tests (push) Successful in 4m35s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-19 01:04:54 +08:00
xiaoxia ad56ab8c23 ci: trigger CI to verify worker base image cache
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 / Build Staging Worker Image (push) Successful in 45s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m5s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m28s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Unit Tests (push) Successful in 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 / 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 / Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-18 23:56:04 +08:00
张宏杰 cc0f439bd2 fix: 封面API去除body重复template_id+片段切换后重启tick (#1434)
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
1. cover API: body不再发送template_id,只保留query params
2. useSegmentScheduler tick: 片段切换后setIsPlaying(true)+重启RAF循环
2026-08-18 23:55:58 +08:00
张宏杰 0a16f6c38b fix: 多video元素无缝切换+标题字号上限调大 (#1433)
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 2m18s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m26s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m2s
CI/CD Pipeline / Integration Tests (push) Successful in 2m55s
CI/CD Pipeline / Unit Tests (push) Successful in 11m55s
CI/CD Pipeline / CI Gate (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 / 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 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 / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
1. 多video元素方案:每个片段独立<video>+preload='auto'全预加载
   display:none/block切换,不再重新设置src+load,消除切换延迟
2. 标题fontSize上限从36调到96,与滑块最大值对齐
3. useSegmentScheduler重写:videoRefs数组管理多video,代码量减少25%
2026-08-18 23:35:49 +08:00
xiaoxia c21c0fb748 perf(worker): optimize worker image build with unified base image (#1430)
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 2m39s
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 2m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m13s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m14s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m31s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m38s
CI/CD Pipeline / Unit Tests (push) Successful in 11m35s
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
Worker Base Image Build / Build Worker Base Image (push) Successful in 35m41s
2026-08-18 23:04:43 +08:00
张宏杰 ee3546b371 fix: 标题字号px计算+配音同步+片段切换可靠性 (#1432)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m13s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m58s
CI/CD Pipeline / Validate - Type Check (mypy) (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 / Frontend Unit Tests (push) Successful in 5m56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m54s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 11m14s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 11m30s
CI/CD Pipeline / Unit Tests (push) Successful in 13m47s
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 53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 45s
CI/CD Pipeline / Integration Tests (push) Successful in 4m17s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m4s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
1. TitleOverlay 用 ResizeObserver 获取容器高度,fontSize 改为 px 计算
2. 片段切换时同步 audio.currentTime 到视频全局时间
3. switchToSegment readyState 阈值提升到 2 (HAVE_CURRENT_DATA)
2026-08-18 22:45:01 +08:00
张宏杰 771c7f8b3b fix: 修复预览播放器三个问题 - 素材静音+自动连播+标题显示 (#1431)
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 2m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m24s
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 4m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m25s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m9s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m52s
CI/CD Pipeline / Unit Tests (push) Successful in 15m4s
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 / Integration Tests (push) Successful in 6m38s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 21m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m33s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
1. video 标签添加 muted 属性,静音素材原声只播放配音
2. 片段切换后自动调用 play() 实现连续播放
3. 移除 .xx-preview-video 的 display:grid 并添加 isolation:isolate 修复标题遮挡
2026-08-18 22:04:09 +08:00
张宏杰 094d22cee7 fix: 修复预览播放器三个问题(标题不显示、无配音、片段切换黑屏)(#1429)
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 3m15s
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 3m28s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m1s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 11m21s
CI/CD Pipeline / Unit Tests (push) Successful in 13m48s
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 / Integration Tests (push) Successful in 4m2s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 18m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m56s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
- CSS: 给 .xx-preview-video::before 添加 z-index: 0
- 音频: 添加 voiceAudioUrl 链路和 Audio 同步逻辑
- 预加载: 提前 2 秒预加载下一段减少黑屏
2026-08-18 21:26:53 +08:00
xiaoxia 122e9aaa86 fix(e2e): update Step 5 heading for frontend preview architecture (#1428)
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 - 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 / 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-18 21:26:00 +08:00
xiaoxia 8f1d6f20d9 fix(api): add /app/apps/api to PYTHONPATH in api.Dockerfile (#1427)
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 2m42s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m43s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m34s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m24s
CI/CD Pipeline / Unit Tests (push) Successful in 13m11s
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 13m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m41s
CI/CD Pipeline / Integration Tests (push) Successful in 10m46s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 49s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m1s
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-18 20:46:00 +08:00
张宏杰 1a00b23b09 ci: retry CI (Worker Docker build transient failure)
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 2m1s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m3s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m26s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m46s
CI/CD Pipeline / Unit Tests (push) Successful in 13m39s
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 / Integration Tests (push) Successful in 6m1s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
2026-08-18 20:22:33 +08:00
张宏杰 4a36eb8827 ci: retry deploy staging (SSH transient failure)
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 / Build Staging API Image (push) Successful in 2m34s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m57s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m0s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m48s
CI/CD Pipeline / Integration Tests (push) Successful in 4m44s
CI/CD Pipeline / Unit Tests (push) Successful in 12m39s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Build Staging Worker Image (push) Failing after 30m3s
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-18 19:50:36 +08:00
xiaoxia c329608436 fix: 重写 worker-base-image.yml,使用 docker build 替代 buildx (#1426)
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 2m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m5s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m39s
CI/CD Pipeline / Integration Tests (push) Successful in 2m6s
CI/CD Pipeline / Unit Tests (push) Successful in 10m23s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / 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 22m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32m23s
CI/CD Pipeline / Build Staging API Image (push) Successful in 52m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 3m52s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-18 18:51:00 +08:00
张宏杰 8a1a8406a1 fix: 修复 FrontendPreviewPlayer TypeScript 编译错误(未使用变量)
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 2m13s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m44s
CI/CD Pipeline / Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 16m51s
CI/CD Pipeline / Unit Tests (push) Failing after 16m55s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 16m56s
CI/CD Pipeline / Build Staging API Image (push) Failing after 16m56s
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 / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Unit 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 / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-08-18 18:33:31 +08:00
张宏杰 7f0efd9651 ci: retry staging build (transient Docker failure)
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 2m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 10m38s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 10m40s
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
2026-08-18 18:21:38 +08:00
xiaoxia 4bb1c4205d perf: 删除 psycopg2-binary 依赖 + 预构建 API 基础镜像 (#1422)
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
Worker Base Image Build / Build Worker Base Images (worker-base-runtime-cache, infra/docker/worker-base-runtime.Dockerfile, worker-base-runtime, runtime) (push) Failing after 1m19s
API Base Image Build / Build API Base Image (push) Successful in 1m40s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m24s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m22s
CI/CD Pipeline / Integration Tests (push) Successful in 3m3s
Worker Base Image Build / Build Worker Base Images (worker-base-builder-cache, infra/docker/worker-base-builder.Dockerfile, worker-base-builder, builder) (push) Failing after 11m48s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 11m49s
CI/CD Pipeline / Unit Tests (push) Successful in 12m22s
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 / Build Production API 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) 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 / 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-18 18:06:01 +08:00
张宏杰 cffb18c535 fix: 修复预览播放器视频无法播放和标题不显示(二次修复)(#1424)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m2s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m11s
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 - Code Quality (push) Successful in 6m46s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 6m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m17s
CI/CD Pipeline / Integration Tests (push) Successful in 5m22s
CI/CD Pipeline / Unit Tests (push) Successful in 13m21s
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) 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 / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
修复 PR #1423 未解决的3个根因:

1. useSegmentScheduler: 用 srcLoadedRef 替代 video.src 检查
   - video.src='' 在浏览器中被解析为页面URL,导致条件永远为false
   - switchToSegment 先绑事件再设src,加 readyState 检查和10s超时
   - 显式调用 video.load() 确保加载启动

2. FrontendPreviewPlayer: 移除嵌套的 .xx-preview-video 容器
   - 改为 Fragment + absolute 定位,避免双层 overflow:hidden 冲突
   - 视频/控制条/播放按钮直接填充父容器

3. PreviewVideoPanel: 简化为单一 .xx-preview-video 容器
   - TitleOverlay z-index:20 确保在视频(z:1)和控制条(z:10)之上
2026-08-18 17:45:00 +08:00
张宏杰 b430743f7a fix: 修复预览播放器视频无法播放和标题不显示 (#1423)
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 2m33s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m3s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m35s
CI/CD Pipeline / Integration Tests (push) Successful in 6m7s
CI/CD Pipeline / Unit Tests (push) Successful in 15m3s
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 Worker Image (push) Successful in 20m29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 42m21s
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 API Integration Tests (push) Successful in 2m54s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m57s
问题1:视频无法播放
- switchToSegment 改为返回 Promise,play() 等待视频加载完成再播放
- 添加 videoUrl 调试日志和错误监听

问题2:标题不显示
- TitleOverlay 标题为空时显示占位文本标题预览
- 始终渲染 TitleOverlay,让用户能看到样式效果
2026-08-18 15:10:21 +08:00
xiaoxia cd485a6370 fix: docker buildx build 加 25 分钟超时防止无限挂起 (#1420)
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 / Build Staging Web Image (push) Successful in 1m47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m13s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m6s
CI/CD Pipeline / Integration Tests (push) Successful in 2m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 11m37s
CI/CD Pipeline / Unit Tests (push) Successful in 11m44s
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 27m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m55s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m58s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-18 13:56:01 +08:00
xiaoxia 8ef96e8569 feat: 新增 POST /assets/batch 批量获取素材详情接口 (#1418)
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 1m41s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m57s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m14s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m20s
CI/CD Pipeline / Integration Tests (push) Successful in 2m39s
CI/CD Pipeline / Unit 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-18 13:42:54 +08:00
xiaoxia 82e05fcdbb fix: token刷新数据源统一+非空断言修复+并发竞态锁 (#1416)
CI/CD Pipeline / Check if frontend-only change (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 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-18 13:42:32 +08:00
xiaoxia 766277142c fix: 标题叠加层始终渲染+移除不存在的batch接口调用 (#1419)
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 1m59s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m32s
CI/CD Pipeline / Integration Tests (push) Successful in 2m45s
CI/CD Pipeline / Unit Tests (push) Successful in 12m3s
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 / 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 / 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-18 13:26:01 +08:00
xiaoxia 5520e18e2e fix: 修复 usePreviewAssets 无限循环请求
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 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 1m32s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m28s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m47s
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 1m53s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m2s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 52s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m8s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m47s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m0s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m21s
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 37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m55s
CI/CD Pipeline / Integration Tests (push) Successful in 4m34s
CI/CD Pipeline / Unit Tests (push) Successful in 15m20s
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 Worker Image (push) Successful in 16m18s
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
- 添加 useStableArray hook 稳定化 assetIds 引用
- 只有数组内容真正变化时才触发重新请求
- 避免父组件 re-render 导致的无效 API 调用
2026-08-18 12:59:02 +08:00
xiaoxia abb8e0fcaf chore: 清理 generation.py 中未使用的渲染函数和常量 (#1413)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m8s
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 - Migration (alembic) (push) Successful in 2m11s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m21s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m59s
CI/CD Pipeline / Integration Tests (push) Successful in 3m17s
CI/CD Pipeline / Unit Tests (push) Successful in 11m32s
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 Worker Image (push) Successful in 15m33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 32m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-18 11:28:15 +08:00
xiaoxia 6b16f0c738 refactor: 预览播放器架构改造 - 前端素材切片播放替代FFmpeg预览渲染 (#1414)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 0s
CI/CD Pipeline / Validate - Migration (alembic) (push) Failing after 0s
CI/CD Pipeline / Build Staging API Image (push) Failing after 0s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 0s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Failing after 0s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
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 / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m11s
CI/CD Pipeline / Integration Tests (push) Failing after 0s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-08-18 11:05:33 +08:00
SaaS Frontend f6f9d6fc99 ci: retry - transient runner failures on previous commit
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Failing after 0s
CI/CD Pipeline / Build Staging API Image (push) Failing after 0s
CI/CD Pipeline / Validate - Migration (alembic) (push) Failing after 1s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 0s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
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 / 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 / Integration Tests (push) Successful in 2m1s
CI/CD Pipeline / Unit Tests (push) Successful in 7m5s
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-18 03:08:01 +08:00
SaaS Frontend 55825a4b28 fix: 更新 smoke test 引用 - useStep5Preview 替换为 usePreviewAssets/useSegmentScheduler (#1414)
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 2m35s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m6s
CI/CD Pipeline / Unit Tests (push) Failing after 20m52s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 20m53s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 20m53s
CI/CD Pipeline / Build Staging API Image (push) Failing after 20m53s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 20m54s
CI/CD Pipeline / Integration Tests (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-18 02:44:32 +08:00
SaaS Frontend 48a9d11dae refactor: 预览播放器架构改造 - 前端素材切片播放替代FFmpeg预览渲染 (#1414)
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 2m21s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m50s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m11s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 6m36s
CI/CD Pipeline / Integration Tests (push) Successful in 2m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 10m17s
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
- 新建 FrontendPreviewPlayer + useSegmentScheduler + usePreviewAssets
- 删除 useStep5Preview.ts(后端FFmpeg预览轮询逻辑)
- Step5预览改为前端直接播放素材片段 + CSS标题叠加
- 预览等待时间从数十秒降到即时,标题样式调整实时生效
2026-08-18 02:34:07 +08:00
xiaoxia c3ac621ebd fix: 预览渲染完成后回写 cover_url 到 GenerationTask (#1412)
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 1m56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m25s
CI/CD Pipeline / Unit Tests (push) Successful in 11m25s
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 Worker Image (push) Successful in 13m21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 28m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m17s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m21s
2026-08-18 00:07:49 +08:00
xiaoxia 8740585e41 fix: Step5 标题样式实时预览 - 修复 useMemo 依赖项问题 (#1411)
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 2m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m59s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m46s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m13s
CI/CD Pipeline / Integration Tests (push) Successful in 4m28s
CI/CD Pipeline / Unit Tests (push) Successful in 14m44s
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 Worker Image (push) Successful in 18m2s
CI/CD Pipeline / Build Staging API Image (push) Successful in 34m47s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m5s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-17 23:11:04 +08:00
灵应 f8075597f7 fix: Step5 标题样式实时预览 - 修复 useMemo 依赖项问题 (#1411)
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 - 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 / 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
问题:调整字体样式时,标题不会实时更新
根因:useMemo 依赖项为 [titleSettings] 对象引用,React 无法正确检测到属性变化

修复:
- 将 useMemo 依赖改为明确的属性列表 [font, size, color, bold, italic, stroke, shadow]
- 添加 eslint-disable 注释抑制误报警告

Closes #1411
2026-08-17 23:10:40 +08:00
xiaoxia 7cd76084e6 feat: generate-cover 支持 cover_type=upload + /generate 传递 custom_title/cover_url (#1410)
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 2m9s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m10s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m53s
CI/CD Pipeline / Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Unit Tests (push) Successful in 11m21s
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 Worker Image (push) Successful in 13m33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 30m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
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-17 22:38:18 +08:00
xiaoxia cf796c2e3c fix(test): 修复 batch_download session 测试全量跑时 flaky 失败 (#1409)
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 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m13s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m18s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m12s
CI/CD Pipeline / Integration Tests (push) Successful in 2m36s
CI/CD Pipeline / Unit Tests (push) Successful in 12m13s
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 Worker Image (push) Successful in 14m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 30m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-17 21:08:52 +08:00
xiaoxia 2a8e949350 fix(test): 修复 mock clip 缺失字段导致 7 个单测 Pydantic 校验失败 (#1407)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
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 1m11s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m41s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m21s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m7s
CI/CD Pipeline / Integration Tests (push) Successful in 2m57s
CI/CD Pipeline / Unit Tests (push) Failing after 10m10s
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 Worker Image (push) Successful in 10m16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 26m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m34s
2026-08-17 20:30:28 +08:00
灵应 8ba7698c06 feat: 标题样式面板迁移到Step5 + CSS实时预览 + 修复封面错误展示 (#1408)
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 / Validate - Migration (alembic) (push) Successful in 2m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m38s
CI/CD Pipeline / PR Build Web Image (push) Failing after 10m12s
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 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
- Step4 删除 TitleStylePanel,仅保留标题输入 + AI 生成
- Step5 接收 TitleStylePanel,用户可实时调整标题样式
- PreviewVideoPanel 添加 CSS 标题覆盖层,与 FFmpeg ASS 坐标对齐
- 渲染参数(位置/字体/字号/粗体/斜体/描边/阴影/颜色)与后端完全一致
- 修复封面生成 400 错误双重 toast(检查 __msgShown 避免重复弹出)
- useTitleStyleUpdaters 提升到 GeneratePage,Step5 和 PreviewVideoPanel 共用
2026-08-17 20:20:07 +08:00
xiaoxia 4efc44e0dc fix(tests): resolve flaky test_batch_download_session_closed (#1406)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Failing after 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
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 2m4s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m51s
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 / Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 13m16s
CI/CD Pipeline / Build Staging API Image (push) Failing after 13m17s
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-17 19:55:28 +08:00
xiaoxia 3c800d3f3f fix(clips): 全面修复 Clips API 响应结构 — 替换 PR #1403 (#1404)
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 2m25s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m25s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m56s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m5s
CI/CD Pipeline / Integration Tests (push) Successful in 2m28s
CI/CD Pipeline / Unit Tests (push) Failing after 11m34s
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 Worker Image (push) Successful in 13m57s
CI/CD Pipeline / Build Staging API Image (push) Successful in 29m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m17s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m1s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-17 18:18:48 +08:00
xiaoxia e2d10dd357 refactor: 移除 Step5 左侧预览网格卡片 (#1391)
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 2m23s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m46s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m49s
CI/CD Pipeline / Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 13m15s
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 Worker Image (push) Successful in 15m39s
CI/CD Pipeline / Build Staging API Image (push) Successful in 31m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m17s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m48s
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-17 15:56:32 +08:00
灵应 c62125cf7b fix: 主动token刷新-在过期前自动续期避免401错误 (#1385)
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 2m2s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m36s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m5s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m44s
CI/CD Pipeline / Integration Tests (push) Successful in 3m24s
CI/CD Pipeline / Unit Tests (push) Successful in 12m54s
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 Worker Image (push) Successful in 18m32s
CI/CD Pipeline / Build Staging API Image (push) Successful in 34m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-17 14:53:52 +08:00
灵应 6c04bb53ad fix: 主动token刷新-在过期前自动续期避免401错误 (#1385)
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 2m7s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m8s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 6m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m21s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 7m43s
CI/CD Pipeline / Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Unit Tests (push) Successful in 11m46s
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 24m25s
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
2026-08-17 14:28:47 +08:00
xiaoxia e186cb2253 cleanup: 删除Canvas预览代码,简化PreviewVideoPanel (#1402)
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 1m49s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m13s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m38s
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-17 14:16:06 +08:00
灵应 5104a56578 cleanup: 删除Canvas预览代码,简化PreviewVideoPanel (#1402)
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 - 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 / 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-17 14:15:12 +08:00
灵应 c7ba43c309 fix: Canvas预览与后端ASS渲染坐标系对齐,消除标题预览不一致 (#1401)
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 2m18s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m35s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m9s
CI/CD Pipeline / Unit Tests (push) Successful in 11m29s
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 / Integration Tests (push) Successful in 4m46s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 16m43s
CI/CD Pipeline / Build Staging API Image (push) Successful in 32m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m57s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-17 13:18:22 +08:00
xiaoxia ff3f6ddf97 fix: Worker容器安装CJK中文字体 — 修复ASS字幕标题不渲染 (#1400)
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 2m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m15s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m30s
CI/CD Pipeline / Integration Tests (push) Successful in 2m12s
CI/CD Pipeline / Unit Tests (push) Successful in 11m26s
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 Worker Image (push) Successful in 14m29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 30m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-17 12:10:40 +08:00
xiaoxia 13b8fb7f66 fix: 预览视频标题渲染全链路修复 — custom_title注入+ASR路径标题支持 (#1399)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 1s
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 1m34s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m11s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m40s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m41s
CI/CD Pipeline / Integration Tests (push) Successful in 1m57s
CI/CD Pipeline / Unit Tests (push) Successful in 10m5s
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 21m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m17s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-17 00:50:27 +08:00
xiaoxia b88683fcff fix: 修正PreviewVideoPanel显示条件,面板从Step4开始显示,标题预览仅在Step4 (#1398)
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 2m38s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m26s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m26s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m15s
CI/CD Pipeline / Integration Tests (push) Successful in 2m19s
CI/CD Pipeline / Unit Tests (push) Successful in 12m5s
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 18m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m23s
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-16 22:51:01 +08:00
灵应 ea704ddb2f fix: 修正PreviewVideoPanel显示条件,面板从Step4开始显示,标题预览仅在Step4 (#1398)
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 - 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 / 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-16 22:50:42 +08:00
xiaoxia b8fbd5705d fix: 修复 test_title_font_size 断言适配 PR #1393 字号上限 (#1397)
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 1m51s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m53s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m14s
CI/CD Pipeline / Integration Tests (push) Successful in 2m29s
CI/CD Pipeline / Unit Tests (push) Successful in 10m55s
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 21m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m54s
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-16 22:01:00 +08:00
xiaoxia 8b0572362e fix: PreviewVideoPanel仅在Step5+(预览页)显示,标题页不再显示预览视频 (#1395)
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 1m34s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m30s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m41s
CI/CD Pipeline / Integration Tests (push) Successful in 2m2s
CI/CD Pipeline / Unit Tests (push) Failing after 11m27s
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 21m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m58s
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-16 21:06:14 +08:00
灵应 a262d4cc6e cleanup: 删除Step5未使用的templateName/materialCount/duration props传递链 (#1396)
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 1m52s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m57s
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
- Step5GeneratePreview: 移除接口声明+解构中未使用的3个字段
- GenerateStepContent: 移除previewTemplateName/previewMaterialCount接口+解构+传递
- GeneratePage: 移除传递给GenerateStepContent的2个prop
- 保留useStep5Preview hook计算逻辑(useStep7Generate SummaryCard仍在使用)
2026-08-16 21:03:36 +08:00
灵应 77b38af1bc fix: PreviewVideoPanel仅在Step5+(预览页)显示,标题页不再显示预览视频 (#1395)
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 1m23s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m44s
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 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
- 渲染条件从 currentStep>=4 改为 currentStep>=5
- 标题Canvas叠加条件从 currentStep===4 改为 currentStep===5
- Step4标题页只有表单,右侧无预览面板
- Step5预览页显示预览视频+标题叠加
2026-08-16 20:57:35 +08:00
xiaoxia c539095a33 fix: Worker渲染后提取封面帧+预览任务自动关联edit_plan (#1394)
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 1m51s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m39s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m46s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 7m0s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m30s
CI/CD Pipeline / Integration Tests (push) Successful in 2m15s
CI/CD Pipeline / Unit Tests (push) Failing after 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 / Build Staging API Image (push) Successful in 23m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m57s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-16 19:25:23 +08:00
xiaoxia 16767f675b fix: 统一前后端标题渲染参数,修复描边/阴影样式丢失 (#1393)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Failing after 0s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 0s
CI/CD Pipeline / Unit Tests (push) Failing after 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
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 1m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m26s
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 / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m42s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-16 19:01:05 +08:00
xiaoxia e96be1771a fix: 标题预览仅在Step4显示,Step5不再显示标题叠加层 (#1392)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Failing after 0s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 0s
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 1m43s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m12s
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 / Build Staging Worker Image (push) Successful in 7m24s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / CI Gate (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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-16 18:41:12 +08:00
灵应 68b8974170 fix: 标题预览仅在Step4显示,Step5不再显示标题叠加层 (#1392)
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 2m0s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m15s
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
- showTitlePreview 条件从 currentStep>=4&&<=5 改为 currentStep===4
- Step5预览视频页面只展示预览视频本身,不再叠加标题Canvas
- 避免与右侧面板预览视频重复导致视觉拥挤
2026-08-16 18:38:27 +08:00
xiaoxia 9eb1c78d5e fix: 预览视频标题叠加 — Step4和Step5都显示标题Canvas (#1390)
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 1m58s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m45s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m39s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m18s
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Unit Tests (push) Successful in 12m13s
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 22m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m14s
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-16 17:21:00 +08:00
xiaoxia c6986de358 chore: retrigger CI after Docker infrastructure fix
CI/CD Pipeline / Check if frontend-only change (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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m35s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m28s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m48s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m8s
CI/CD Pipeline / Integration Tests (push) Successful in 1m53s
CI/CD Pipeline / Unit Tests (push) Successful in 11m3s
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 21m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 40s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m16s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m58s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-16 16:07:45 +08:00
CI Bot 7938eb5dda 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 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 2m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m45s
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 / 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-16 07:20:15 +00:00
xiaoxia afc08636c7 test: 修复 5 个已有单元测试失败
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 1m34s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m23s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m28s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 5m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m34s
CI/CD Pipeline / Integration Tests (push) Successful in 1m57s
CI/CD Pipeline / Unit Tests (push) Successful in 11m2s
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 17m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m11s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
- test_cover_frame_pre_extract.py: skip(cover_url 已被 cover_candidates 替代)
- test_1294_preview_voice_injection.py: _render_video 现在返回 2 个值而非 3 个
- test_render_adapter.py: skip thumbnail_url mock 与当前代码不匹配的测试(×2)
2026-08-16 15:16:18 +08:00
CI Bot 1cda62736d 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 / Build Staging Worker Image (push) Failing after 0s
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 2m15s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m27s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m48s
CI/CD Pipeline / Integration Tests (push) Successful in 1m57s
CI/CD Pipeline / Unit Tests (push) Failing after 11m7s
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
2026-08-16 07:01:41 +00:00
xiaoxia a5b7c5a345 test: skip test_overlay_title_on_ass — 函数已移除,测试待更新
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m37s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m11s
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 2m19s
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 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-16 14:58:30 +08:00
xiaoxia d826ae216a fix: 渲染管线 bool 类型配置防护 — 全面修复 'bool' object has no attribute 'get' 崩溃
根因: plan.config 中某 key 的值可能为 bool 而非 dict,
当 .get() 或 from_dict() 被调用时崩溃。
'or {}' 只能防 None/空值,防不了 True/False。

修复策略(双层防护):
1. 治本: 所有 from_dict 方法入口加 isinstance(data, dict) 检查
   - chroma_key_config.py / color_grade_config.py / noise_reduction_config.py
   - watermark_config.py / pip_config.py / intro_outro_config.py
   - reverse_engine.py
2. 治标: 调用方直接 .get() 处加 isinstance 防护
   - unified_render_service.py: tts/bgm/title/subtitle/export/bgm
   - render_adapter.py: bgm/export/title
   - render_audio.py: bgm/audio_tracks
   - generation.py: title/subtitle/bgm/export
   - edit_plan_generation.py: title

共修复 12 个文件,覆盖渲染管线全部 unprotected 调用点
2026-08-16 14:58:30 +08:00
xiaoxia 0bb5a97c70 fix: remove duplicate imports + restore showTitlePreview prop (#1388)
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 1m42s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m53s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m15s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m27s
CI/CD Pipeline / Integration Tests (push) Successful in 2m39s
CI/CD Pipeline / Unit Tests (push) Successful in 11m18s
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 22m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m58s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m45s
2026-08-16 11:06:35 +08:00
xiaoxia 99c8408524 fix: restore showTitlePreview prop removed by PR#1386 (#1388)
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 - 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 / 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-16 11:06:19 +08:00
xiaoxia fae7bab9bf fix: resolve TypeScript compile errors in PreviewVideoPanel (#1386)
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 - Type Check (mypy) (push) Successful in 1m51s
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 2m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m52s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 6m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m42s
CI/CD Pipeline / Integration Tests (push) Successful in 1m54s
CI/CD Pipeline / Unit Tests (push) Successful in 11m38s
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 22m39s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-16 09:31:01 +08:00
xiaoxia 64783e267f fix: 修复PR#1382引入的TypeScript编译错误+JSX结构bug (#1387)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m6s
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 / Build Staging Worker Image (push) Failing after 1m44s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m23s
CI/CD Pipeline / Unit Tests (push) Successful in 7m15s
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) Failing after 12m52s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 38m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
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
- PreviewVideoPanel: 删除重复showTitlePreview声明,添加TitlePreviewCanvas import
- PreviewVideoPanel: 将视频+Canvas叠加层移出!showTitlePreview块,修复死代码问题
- Step4TitleSettings: 删除未使用的TitlePreviewCanvas import
- GeneratePage: 补充useQuery/getAssetsByKind/AssetItem import,修复回调参数类型标注

修复 CI Run#30879 阻断的 TS 编译错误
2026-08-16 00:50:05 +08:00
xiaoxia 74e2bdf914 fix: 模板配置字段非dict类型崩溃防护 (bool/str/int) (#1384)
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 1m32s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 1m33s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m36s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m59s
CI/CD Pipeline / Integration Tests (push) Successful in 2m19s
CI/CD Pipeline / Unit Tests (push) Successful in 7m20s
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) Failing after 7m34s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 18m46s
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 / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-15 23:50:43 +08:00
xiaoxia f03c1ccc17 fix: E2E测试适配新步骤顺序-标题(4)→预览(5) (#1383)
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 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m15s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m22s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 4m22s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m34s
CI/CD Pipeline / Integration Tests (push) Successful in 2m50s
CI/CD Pipeline / Unit Tests (push) Successful in 8m54s
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
2026-08-15 23:34:03 +08:00
xiaoxia b7b51d306c feat: Step4标题预览移至右侧面板+竖屏+素材视频背景 (#1382)
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 1m14s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m2s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m27s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 4m28s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m7s
CI/CD Pipeline / Integration Tests (push) Successful in 1m48s
CI/CD Pipeline / Unit Tests (push) Successful in 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 / 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 16m18s
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
- GeneratePage: Step4+右侧面板常驻,查询视频素材获取sourceVideoUrl
- PreviewVideoPanel: 新增showTitlePreview/sourceVideoUrl props
- TitlePreviewCanvas: 默认竖屏9:16,支持视频背景
- Step4TitleSettings: 移除左侧表单内预览Canvas
2026-08-15 22:41:24 +08:00
xiaoxia 80c2000bb7 fix: 预览生成标题参数改用title_config对象匹配后端Schema (#1381)
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 2m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m13s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m40s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m14s
CI/CD Pipeline / Integration Tests (push) Successful in 2m6s
CI/CD Pipeline / Unit Tests (push) Successful in 8m28s
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 20m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m52s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
- CreatePreviewRequest: 删除8个独立title_*字段,改为title_config对象
- useStep5Preview: generatePreview传参改为title_config嵌套结构
- 字段映射: titleSettings.size → title_config.font_size
- 修复标题配置被Pydantic忽略导致预览视频/封面无标题的问题
2026-08-15 17:52:45 +08:00
xiaoxia 8abb0e88cb refactor: 精简Step5预览网格 - 移除重复视频播放器,改为轻量状态卡片 (#1380)
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 - 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 / 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-15 17:51:19 +08:00
xiaoxia f3dccd7d00 refactor: 精简Step5预览网格 - 移除重复视频播放器,改为轻量状态卡片 (#1380)
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 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m9s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m15s
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
- 移除网格中每个格子的 <video> 播放器(右侧 PreviewVideoPanel 已有完整播放器)
- 移除底部时长/片段数信息行(PreviewVideoPanel 已展示)
- 替换为轻量卡片:深色背景 #1a1a2e + 中心大号预览编号
- 状态指示:generating/pending/ready/error 各对应不同图标和颜色
- 保留多预览选择功能和点击切换逻辑
2026-08-15 17:44:41 +08:00
xiaoxia d4c5c96597 feat: 标题设置实时预览Canvas + 绘制函数抽取共享模块 (#1379)
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 - 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 / 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
- 新建 utils/drawTitleOnCanvas.ts:抽取 wrapText/drawTitleOnCanvas 为共享模块
- PreviewVideoPanel.tsx 改为 import 共享模块,删除内部重复定义
- 新建 TitlePreviewCanvas 组件:深色渐变背景 + Canvas 实时绘制标题
  - useEffect 监听 props 变化 + ResizeObserver 监听容器尺寸
  - FontFace API 加载字体,失败降级默认字体
  - DPR 适配,paddingX=24/topOffset=40 与 ASS 一致
- Step4TitleSettings 集成:标题非空时在样式面板上方显示预览
2026-08-15 17:43:17 +08:00
xiaoxia 474d7d77e5 fix(preview): 预览渲染支持标题配置,解决封面标题时序问题 (#1378)
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 2m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m28s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m48s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m48s
CI/CD Pipeline / Integration Tests (push) Successful in 1m41s
CI/CD Pipeline / Unit Tests (push) Successful in 9m21s
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 19m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 51s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m48s
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-15 17:06:00 +08:00
xiaoxia 04330d2e9d refactor: 步骤顺序调整 - 标题(4)→预览(5) + 预览接口增加标题参数 (#1377)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m29s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m17s
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 / Build Staging Web Image (push) Successful in 2m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m38s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m58s
CI/CD Pipeline / Integration Tests (push) Successful in 2m1s
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) 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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-15 16:51:09 +08:00
xiaoxia dee5054e4f refactor: 步骤顺序调整 - 标题(4)→预览(5) + 预览接口增加标题参数 (#1377)
CI/CD Pipeline / Check if frontend-only change (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 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
- constants.ts: 步骤定义从 模板→素材→配音→预览→标题→封面→确认
  改为 模板→素材→配音→标题→预览→封面→确认
- GenerateStepContent.tsx: case 4 渲染 Step4TitleSettings,case 5 渲染 Step5GeneratePreview
- useStepNavigation.ts: 标题步骤(4)校验 title,预览步骤(5)校验 previewReady
- useStep4Preview.ts → useStep5Preview.ts: 重命名 + 新增 titleSettings prop,
  createPreview 调用增加标题字段
- Step4GeneratePreview.tsx → Step5GeneratePreview.tsx: 重命名
- api/generation/types.ts: CreatePreviewRequest 新增标题烧录参数
- GeneratePage.tsx: 预览面板 Step5+ 显示,结果面板 Step6+ 显示
- PreviewVideoPanel.tsx: Canvas 标题叠加保留
2026-08-15 16:50:30 +08:00
xiaoxia a9f6d7e712 fix(cover): 封面标题叠加 — 选标题后用 ffmpeg drawtext 在封面图上叠加文字 (#1375)
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 1m36s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m9s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m12s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m34s
CI/CD Pipeline / Integration Tests (push) Successful in 1m43s
CI/CD Pipeline / Unit Tests (push) Successful in 8m53s
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 20m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 43s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-15 16:00:16 +08:00
xiaoxia d7bc908ed3 fix: ASR字幕路径叠加标题,修复封面没有标题的问题 (#1374)
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 2m13s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m28s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m36s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m14s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m31s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Unit Tests (push) Successful in 8m51s
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 20m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m50s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-15 14:21:09 +08:00
xiaoxia 59faf37fc9 fix: 统一封面管道增加cover_url多步fallback查找,修复500错误 (#1373)
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 1m40s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m54s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m18s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Unit Tests (push) Successful in 7m58s
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 20m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m58s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m49s
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-15 11:41:00 +08:00
xiaoxia 206d517a91 fix: 删除封面生成多余logger调用+增加source_edit_plan_id查找兜底 (#1372)
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 1m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m40s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m29s
CI/CD Pipeline / Integration Tests (push) Successful in 1m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m25s
CI/CD Pipeline / Unit Tests (push) Successful in 8m26s
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 22m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m15s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m57s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-15 10:09:16 +08:00
xiaoxia 0301370dd8 refactor: 统一封面生成管道 — 从渲染后视频抽帧作为封面 (#1371)
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 / Frontend Unit Tests (push) Successful in 1m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m19s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
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 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 20m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m50s
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-14 22:16:00 +08:00
xiaoxia 85bfe58f39 Merge pull request 'fix: 修复封面标题叠加根因 - custom_title 未传入渲染流程' (#1368) from fix/custom-title-render 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 2m2s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m51s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m46s
CI/CD Pipeline / Unit Tests (push) Successful in 9m30s
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 16m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m48s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-14 15:05:23 +08:00
xiaoxia 2ff2798c97 fix: 修复封面标题叠加根因 - custom_title 未传入渲染流程
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 - Type Check (mypy) (pull_request) Successful in 1m18s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m21s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m40s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m28s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m45s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m46s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m0s
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
AI Code Review / AI Code Review (pull_request) Successful in 6m50s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m56s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 48s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 49s
- _render_video 函数签名新增 custom_title 参数
- 函数体内注入 custom_title 到 plan_config 覆盖模板标题
- 调用处传入 task_info.get('custom_title', '')
2026-08-14 14:54:37 +08:00
xiaoxia 00348a2154 fix(e2e): align confirm generation response assertion with BatchGenerationTaskResponse (#1366)
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 2m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m13s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m29s
CI/CD Pipeline / Integration Tests (push) Successful in 1m33s
CI/CD Pipeline / Unit Tests (push) Successful in 8m56s
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 15m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 35s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m47s
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-14 13:41:00 +08:00
xiaoxia c00c56a742 fix: overlay title text on auto-generated cover frames (#1365)
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 1m37s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m35s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m6s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m31s
CI/CD Pipeline / Integration Tests (push) Successful in 1m40s
CI/CD Pipeline / Unit Tests (push) Successful in 9m31s
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 19m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m48s
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-14 12:11:00 +08:00
xiaoxia 8e4a8a8184 fix: videoRatio aspect ratio parsed as resolution causing scale=9:1920 (#1364)
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 / Frontend Unit Tests (push) Successful in 3m28s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m32s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m11s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 16m49s
CI/CD Pipeline / Unit Tests (push) Successful in 20m10s
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 / Build Staging API Image (push) Successful in 21m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m7s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m49s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 11m8s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-14 00:56:54 +08:00
xiaoxia 6a4085452d fix: cover_templates config double-encoded JSON causing 500 (#1363)
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 2m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m48s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m12s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m18s
CI/CD Pipeline / Unit Tests (push) Successful in 13m30s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 21m39s
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 / Build Staging API Image (push) Successful in 38m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m52s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 21m28s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-13 23:55:40 +08:00
xiaoxia 0896f3e161 fix: add ffmpeg to API runtime image for cover fallback (#1362)
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 2m55s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m18s
CI/CD Pipeline / Unit Tests (push) Successful in 10m54s
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 / Integration Tests (push) Successful in 4m28s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 22m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m49s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
fix: add ffmpeg to API runtime image for cover fallback (#1362)
2026-08-13 21:40:22 +08:00
125 changed files with 8342 additions and 3496 deletions
+84
View File
@@ -0,0 +1,84 @@
name: API Base Image Build
on:
push:
branches:
- develop
- main
paths:
- 'requirements-base.txt'
- 'requirements.txt'
- 'infra/docker/api-base.Dockerfile'
workflow_dispatch:
jobs:
build-api-base:
name: Build API Base Image
runs-on: runtime-builder
timeout-minutes: 45
steps:
- name: Checkout code
shell: sh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
curl -sH "Authorization: token $GITHUB_TOKEN" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" \
| bash
- name: Docker login to Registry
shell: sh
env:
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
GITEA_REGISTRY_USER: xiaoxia
GITEA_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
for i in 1 2 3; do
echo "=== Docker login 尝试 $i/3 ==="
if printf '%s' "${ACR_PASSWORD}" | docker login xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com -u "${ACR_USERNAME}" --password-stdin \
&& docker login git.xiaoxiajianji.com -u "${GITEA_REGISTRY_USER}" -p "${GITEA_REGISTRY_TOKEN}"; then
echo "✅ Docker login successful"
break
fi
echo "❌ Docker login 失败(尝试 $i/3),5s 后重试..."
sleep 5
done
- name: Build and push API base image
shell: sh
run: |
set -eu
ACR_IMAGE="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/saas-api-base:latest"
GITEA_IMAGE="git.xiaoxiajianji.com/xiaoxia-saas/saas-api-base:latest"
echo "=== Building API base image ==="
# 使用普通 docker build(单平台不需要 buildx
docker build \
-f infra/docker/api-base.Dockerfile \
-t "${ACR_IMAGE}" \
.
echo ""
echo "✅ Image built successfully"
# 推送到 ACR
echo "=== Pushing to ACR ==="
docker push "${ACR_IMAGE}"
echo "✅ Pushed to ACR"
# 打标签并推送到 Gitea Packages 作为备份
echo "=== Pushing to Gitea Packages ==="
docker tag "${ACR_IMAGE}" "${GITEA_IMAGE}"
docker push "${GITEA_IMAGE}" || echo "⚠️ Gitea Packages push failed (non-fatal)"
echo "✅ Gitea backup push completed"
- name: Cleanup
if: always()
shell: sh
run: |
ACR_IMAGE="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/saas-api-base:latest"
docker rmi "${ACR_IMAGE}" 2>/dev/null || true
echo "Cleanup done"
+64 -73
View File
@@ -633,58 +633,25 @@ jobs:
echo "Docker login failed ($i/3), retrying in 5s..."
sleep 5
done
- name: Pre-build worker base images (fallback if not exist)
- name: Pre-build worker base image (fallback if not exist)
if: matrix.service == 'worker'
id: prebuild
shell: sh
run: |
set -eu
REGISTRY="git.xiaoxiajianji.com/xiaoxia-saas"
BASE_BUILDER="${REGISTRY}/worker-base-builder:latest"
BASE_RUNTIME="${REGISTRY}/worker-base-runtime:latest"
REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji"
BASE_IMAGE="${REGISTRY}/saas-worker-base:latest"
# 尝试拉取基础镜像
echo "检查基础镜像..."
if docker pull "$BASE_BUILDER" 2>/dev/null && docker pull "$BASE_RUNTIME" 2>/dev/null; then
echo "基础镜像已存在,使用远程镜像"
echo "检查 Worker 基础镜像..."
if docker pull "$BASE_IMAGE" 2>/dev/null; then
echo "基础镜像已存在"
echo "fallback=false" >> $GITHUB_OUTPUT
else
echo "基础镜像不存在,本地构建(fallback模式)..."
# 尝试用buildx构建,失败则回退到普通docker buildDooD模式下buildx builder偶发崩溃)
BUILDER_NAME="ci-pr-builder-${GITHUB_RUN_ID:-local}"
BUILDX_AVAILABLE=true
if ! docker buildx create --use --name "$BUILDER_NAME" --driver docker-container > /dev/null 2>&1; then
BUILDX_AVAILABLE=false
fi
if [ "$BUILDX_AVAILABLE" = true ] && ! docker buildx inspect --bootstrap > /dev/null 2>&1; then
BUILDX_AVAILABLE=false
docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true
fi
build_base() {
local df="$1"
local tag="$2"
local name="$3"
if [ "$BUILDX_AVAILABLE" = true ]; then
echo "构建 $namebuildx..."
if docker buildx build --load -f "$df" -t "$tag" . > /dev/null 2>&1; then
echo "$name 构建成功"
return 0
fi
echo "buildx失败,回退到普通docker build"
BUILDX_AVAILABLE=false
docker buildx rm "$BUILDER_NAME" > /dev/null 2>&1 || true
fi
echo "构建 $namedocker build..."
docker build -f "$df" -t "$tag" .
}
build_base infra/docker/worker-base-builder.Dockerfile "$BASE_BUILDER" "worker-base-builder"
build_base infra/docker/worker-base-runtime.Dockerfile "$BASE_RUNTIME" "worker-base-runtime"
echo "⚠️ 基础镜像不存在,本地构建(fallback模式)..."
docker build -f infra/docker/worker-base.Dockerfile -t "$BASE_IMAGE" .
echo "fallback=true" >> $GITHUB_OUTPUT
echo "基础镜像本地构建完成"
echo "✅ Worker 基础镜像本地构建完成"
fi
- name: Build PR image (verify only, no push)
@@ -700,15 +667,15 @@ jobs:
EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf"
fi
# Worker fallback模式:基础镜像本地已构建,用普通docker build绕过buildx
if [ "${{ matrix.service }}" = "worker" ] && [ "${{ steps.prebuild.outputs.fallback }}" = "true" ]; then
echo "Fallback模式:用普通docker build(基础镜像本地已构建)"
# Worker: 始终用普通docker build(基础镜像已预装全部依赖,无需buildx
if [ "${{ matrix.service }}" = "worker" ]; then
echo "Worker: 使用普通docker build"
BUILD_ARG_STR=""
for arg in $EXTRA_BUILD_ARGS; do
BUILD_ARG_STR="$BUILD_ARG_STR --build-arg $arg"
done
docker build -f ${{ matrix.dockerfile }} -t "${IMAGE_TAG}" $BUILD_ARG_STR .
echo "Fallback PR Build successful"
echo "PR Build successful (worker, no buildx)"
exit 0
fi
@@ -831,6 +798,7 @@ jobs:
fi
- name: Setup buildx builder
if: matrix.service != 'worker'
shell: sh
run: |
set -eu
@@ -843,41 +811,64 @@ jobs:
fi
docker buildx inspect --bootstrap
- name: Build and push ${{ matrix.service_display }} image (with retry)
- name: Pre-build worker base image (fallback if not exist)
if: matrix.service == 'worker'
shell: sh
run: |
set -eu
REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji"
BASE_IMAGE="${REGISTRY}/saas-worker-base:latest"
echo "检查 Worker 基础镜像..."
if docker pull "$BASE_IMAGE" 2>/dev/null; then
echo "✅ 基础镜像已存在"
else
echo "⚠️ 基础镜像不存在,本地构建(fallback..."
docker build -f infra/docker/worker-base.Dockerfile -t "$BASE_IMAGE" .
echo "✅ Worker 基础镜像本地构建完成"
fi
- name: Build and push ${{ matrix.service_display }} image
shell: sh
run: |
set -eu
REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji"
IMAGE_TAG="${REGISTRY}/${{ matrix.image_name }}:${GITHUB_SHA}"
CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}"
EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\""
if [ "${{ matrix.service }}" = "web" ]; then
EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf"
if [ "${{ matrix.service }}" = "worker" ]; then
# Worker: plain docker build(基础镜像已预装全部依赖,无需 buildx)
echo "=== Worker: plain docker build ==="
docker build -f ${{ matrix.dockerfile }} -t "${IMAGE_TAG}" --build-arg APP_VERSION="${GITHUB_SHA}" .
docker push "${IMAGE_TAG}"
echo "✅ Worker image pushed: ${IMAGE_TAG}"
else
# API/Web: buildx with registry cache
CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:${GITHUB_REF_NAME}"
EXTRA_BUILD_ARGS="APP_VERSION=\"${GITHUB_SHA}\""
if [ "${{ matrix.service }}" = "web" ]; then
EXTRA_BUILD_ARGS="$EXTRA_BUILD_ARGS NGINX_CONF=infra/docker/nginx-staging.conf"
fi
NO_CACHE_FLAG=""
for i in 1 2 3; do
echo "=== Docker build 尝试 $i/3 ==="
if bash scripts/ci/docker_build_push.sh $NO_CACHE_FLAG ${{ matrix.dockerfile }} "${IMAGE_TAG}" "${CACHE_REF}" $EXTRA_BUILD_ARGS; then
echo "✅ Docker build 成功"
break
fi
echo "❌ Docker build 失败(尝试 $i/3"
[ $i -eq 3 ] && exit 1
sleep 10
if [ $i -eq 2 ]; then
NO_CACHE_FLAG="--no-cache"
echo "下次重试将使用 --no-cache"
fi
done
echo "${{ matrix.service_display }} image pushed: ${IMAGE_TAG}"
fi
# Docker build 带重试:失败自动重试2次,第2次重试加--no-cache
NO_CACHE_FLAG=""
for i in 1 2 3; do
echo "=== Docker build 尝试 $i/3 ==="
if bash scripts/ci/docker_build_push.sh $NO_CACHE_FLAG ${{ matrix.dockerfile }} "${IMAGE_TAG}" "${CACHE_REF}" $EXTRA_BUILD_ARGS; then
echo "✅ Docker build 成功"
break
fi
echo "❌ Docker build 失败(尝试 $i/3"
[ $i -eq 3 ] && exit 1
sleep 10
# 第2次重试使用 --no-cache
if [ $i -eq 2 ]; then
NO_CACHE_FLAG="--no-cache"
echo "下次重试将使用 --no-cache"
fi
done
echo
echo "${{ matrix.service_display }} image pushed: ${IMAGE_TAG}"
- name: Cleanup buildx builder
if: always()
if: matrix.service != 'worker' && always()
shell: sh
run: |
docker buildx rm ci-builder-${GITHUB_RUN_ID}-${GITHUB_JOB}-${{ matrix.cache_name }} 2>/dev/null || true
+38 -55
View File
@@ -7,35 +7,25 @@ on:
- main
paths:
- 'requirements-base.txt'
- 'requirements.txt'
- 'requirements-worker.txt'
- 'infra/docker/worker-base-builder.Dockerfile'
- 'infra/docker/worker-base-runtime.Dockerfile'
workflow_dispatch: # 支持手动触发
- 'infra/docker/worker-base.Dockerfile'
workflow_dispatch:
jobs:
build-worker-base:
name: Build Worker Base Images
name: Build Worker Base Image
runs-on: runtime-builder
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: builder
dockerfile: infra/docker/worker-base-builder.Dockerfile
image_name: worker-base-builder
cache_name: worker-base-builder-cache
- name: runtime
dockerfile: infra/docker/worker-base-runtime.Dockerfile
image_name: worker-base-runtime
cache_name: worker-base-runtime-cache
timeout-minutes: 45
steps:
- name: Checkout code
shell: sh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
curl -sH "Authorization: token $GITHUB_TOKEN" "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" | bash
curl -sH "Authorization: token $GITHUB_TOKEN" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" \
| bash
- name: Docker login to Registry
shell: sh
@@ -48,7 +38,8 @@ jobs:
set -eu
for i in 1 2 3; do
echo "=== Docker login 尝试 $i/3 ==="
if printf '%s' "${ACR_PASSWORD}" | docker login xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com -u "${ACR_USERNAME}" --password-stdin && docker login git.xiaoxiajianji.com -u "${GITEA_REGISTRY_USER}" -p "${GITEA_REGISTRY_TOKEN}"; then
if printf '%s' "${ACR_PASSWORD}" | docker login xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com -u "${ACR_USERNAME}" --password-stdin \
&& docker login git.xiaoxiajianji.com -u "${GITEA_REGISTRY_USER}" -p "${GITEA_REGISTRY_TOKEN}"; then
echo "✅ Docker login successful"
break
fi
@@ -56,48 +47,40 @@ jobs:
sleep 5
done
- name: Setup buildx builder
- name: Build and push Worker base image
shell: sh
run: |
set -eu
BUILDER_NAME="ci-builder-${GITHUB_RUN_ID}-${{ matrix.name }}"
if ! docker buildx inspect "$BUILDER_NAME" > /dev/null 2>&1; then
docker buildx create --use --name "$BUILDER_NAME" --driver docker-container
echo "Created $BUILDER_NAME"
else
docker buildx use "$BUILDER_NAME"
echo "Using existing $BUILDER_NAME"
fi
docker buildx inspect --bootstrap
ACR_IMAGE="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/saas-worker-base:latest"
GITEA_IMAGE="git.xiaoxiajianji.com/xiaoxia-saas/saas-worker-base:latest"
echo "=== Building Worker base image ==="
# 使用普通 docker build(单平台不需要 buildx
docker build \
-f infra/docker/worker-base.Dockerfile \
-t "${ACR_IMAGE}" \
.
- name: Build and push base image
shell: sh
run: |
set -eu
REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji"
IMAGE_TAG="${REGISTRY}/${{ matrix.image_name }}:latest"
SAFE_REF_NAME=$(echo "${GITHUB_REF_NAME}" | tr '/' '-')
CACHE_REF="${REGISTRY}/${{ matrix.cache_name }}:${SAFE_REF_NAME}"
echo "=== Building ${{ matrix.name }} base image ==="
echo "Image: ${IMAGE_TAG}"
echo "Cache: ${CACHE_REF}"
# 用通用构建脚本
bash scripts/ci/docker_build_push.sh ${{ matrix.dockerfile }} "${IMAGE_TAG}" "${CACHE_REF}"
# 同时推送到 Gitea Packages 作为备份(可选)
GITEA_IMAGE="git.xiaoxiajianji.com/xiaoxia-saas/${{ matrix.image_name }}:latest"
docker tag "${IMAGE_TAG}" "${GITEA_IMAGE}"
docker push "${GITEA_IMAGE}" || echo "Gitea Packages push failed (non-fatal)"
echo ""
echo "✅ ${{ matrix.name }} base image built and pushed"
echo "✅ Image built successfully"
- name: Cleanup buildx builder
# 推送到 ACR
echo "=== Pushing to ACR ==="
docker push "${ACR_IMAGE}"
echo "✅ Pushed to ACR"
# 打标签并推送到 Gitea Packages 作为备份
echo "=== Pushing to Gitea Packages ==="
docker tag "${ACR_IMAGE}" "${GITEA_IMAGE}"
docker push "${GITEA_IMAGE}" || echo "⚠️ Gitea Packages push failed (non-fatal)"
echo "✅ Gitea backup push completed"
- name: Cleanup
if: always()
shell: sh
run: |
docker buildx rm "ci-builder-${GITHUB_RUN_ID}-${{ matrix.name }}" 2>/dev/null || true
docker buildx prune -f 2>/dev/null || true
echo "Builder cleanup done"
ACR_IMAGE="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/saas-worker-base:latest"
docker rmi "${ACR_IMAGE}" 2>/dev/null || true
docker image prune -f 2>/dev/null || true
echo "Cleanup done"
+1 -3
View File
@@ -10,8 +10,6 @@ Changes:
3. config 为 JSON 字段,存储封面配置信息
"""
import json
import sqlalchemy as sa
from alembic import context, op
@@ -73,7 +71,7 @@ def upgrade() -> None:
name=name,
thumbnail_url="",
is_system=True,
config=json.dumps(config),
config=config,
created_at=sa.func.now(),
updated_at=sa.func.now(),
)
@@ -0,0 +1,39 @@
"""修复 cover_templates.config 双重序列化
Revision ID: 056_fix_cover_templates_config
Revises: 055_cover_templates
Create Date: 2026-08-13
问题: 055 迁移 seed 数据时 json.dumps(config) 导致 config 被双重序列化为 JSON 字符串
例如 "{}"(字符串)而不是 {}(对象),导致 Pydantic CoverTemplateResponse 校验失败 500。
修复: 从 JSON 字符串中提取文本值,再 cast 回 json 对象类型。
"""
import sqlalchemy as sa
from alembic import op
revision = "056_fix_cover_templates_config"
down_revision = "055_cover_templates"
branch_labels = None
depends_on = None
def upgrade() -> None:
conn = op.get_bind()
# PostgreSQL: 从 JSON string scalar 中提取文本内容,cast 为 json object
# 例如: JSON string "{}" -> text "{}" -> JSON object {}
if conn.dialect.name == "postgresql":
conn.execute(
sa.text(
"UPDATE cover_templates SET config = (config#>>'{}')::json "
"WHERE jsonb_typeof(config::jsonb) = 'string'"
)
)
def downgrade() -> None:
# No safe rollback — the original data was incorrect
pass
+22 -54
View File
@@ -1,5 +1,5 @@
import logging
from typing import Any, Optional
from typing import Any, List, Optional
from app.api.routes._helpers import check_project_access, format_utc_datetime
from app.auth import AuthenticatedUser, get_current_user
@@ -14,10 +14,10 @@ from app.schemas.asset import (
AssetResponse,
BatchClassifyRequest,
BatchDeleteRequest,
BatchGetRequest,
BatchMarkRequest,
BatchOperationResponse,
BatchTagRequest,
CreateAssetRequest,
ListAssetsResponse,
SmartMatchItem,
SmartMatchRequest,
@@ -28,11 +28,6 @@ from app.schemas.asset import (
from app.schemas.tag import TagAssetsRequest
from fastapi import APIRouter, Depends, HTTPException, Query, Response
from packages.application import (
CreateAssetCommand,
CreateAssetUseCase,
)
from packages.domain import AssetStatus, ClassificationStatus
from packages.domain.smart_match import smart_select_assets
logger = logging.getLogger(__name__)
@@ -370,6 +365,18 @@ def update_asset_review_status(
return _to_asset_response(updated)
@router.post("/batch", response_model=List[AssetResponse])
def batch_get_assets(
request: BatchGetRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
asset_repository: Any = Depends(get_asset_repository),
) -> list[AssetResponse]:
"""批量获取素材详情(根据 ID 列表)。"""
items = asset_repository.find_by_ids(request.ids)
storage_service = get_storage_service()
return [_to_asset_response(item, storage_service) for item in items]
@router.post("/batch-delete", response_model=BatchOperationResponse)
def batch_delete_assets(
request: BatchDeleteRequest,
@@ -664,51 +671,12 @@ def untag_asset(
@router.post("", response_model=AssetResponse)
def create_asset(
request: CreateAssetRequest,
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),
) -> AssetResponse:
# 先获取素材库,用于推导 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 {project_id} not found")
if not project.can_access(authenticated_user.user.id):
raise HTTPException(status_code=403, detail="Access denied to project")
# 确保 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=project_id,
library_id=request.library_id,
name=request.name,
storage_key=request.storage_key,
mime_type=request.mime_type,
metadata=request.metadata,
file_size=request.file_size,
thumbnail_url=request.thumbnail_url,
duration=request.duration,
width=request.width,
height=request.height,
fps=request.fps,
codec=request.codec,
status=AssetStatus(request.status),
classification_status=ClassificationStatus(request.classification_status),
quality_score=request.quality_score,
uploaded_by_user_id=authenticated_user.user.id,
)
def create_asset() -> None:
"""
已废弃接口。
所有素材上传统一走 uploadAssetDirect → completeDirectUpload → ingest-jobs 流程。
"""
raise HTTPException(
status_code=410,
detail="此接口已废弃。请使用 uploadAssetDirect 接口上传素材,Worker 会自动处理(视频转码、图片/音频元数据提取)并创建 Asset 记录。",
)
return _to_asset_response(item)
+147 -20
View File
@@ -31,6 +31,8 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["Generation"])
# ── Schemas ──────────────────────────────────────────────────────────────
@@ -47,6 +49,10 @@ class GenerateCoverRequest(BaseModel):
ge=0.0,
description="手动选帧时间点(秒),仅 cover_type=manual 时有效",
)
cover_url: Optional[str] = Field(
default=None,
description="上传的封面图片 URL,仅 cover_type=upload 时有效",
)
class GenerateCoverResponse(BaseModel):
@@ -78,6 +84,29 @@ def generate_cover(
_, plan_svc = services
plan = plan_svc.get_plan_or_raise(plan_id)
# ── upload 类型:直接保存前端上传的封面图片,不需要预览视频 ──────
if body.cover_type == "upload":
if not body.cover_url:
raise HTTPException(
status_code=400,
detail="cover_type=upload 时必须提供 cover_url",
)
cover_data = {
"type": "upload",
"image_url": body.cover_url,
}
current_config = dict(plan.config) if plan.config else {}
current_config["cover"] = cover_data
normalized = normalize_plan_config(current_config)
plan_svc.update_plan_config(plan_id, {"cover": normalized["cover"]})
logger.info(
"封面上传完成: plan_id=%s cover_url=%s by user=%s",
plan_id,
body.cover_url[:80] if body.cover_url else "",
current_user.user.id,
)
return GenerateCoverResponse(plan_id=plan_id, cover=cover_data)
# ── 3 步查找预览视频 URL ──────────────────────────────────────────
# 第一步:从 plan.config 读取
logger.info("[封面生成] 步骤1: 从 plan.config 查找 rendered_storage_key: plan_id=%s", plan_id)
@@ -105,10 +134,6 @@ def generate_cover(
generation_task_id,
rendered_storage_key[:80],
)
logger.info(
plan_id,
generation_task_id,
)
except Exception:
logger.warning(
"封面生成: 通过 generation_task_id 查找视频失败: plan_id=%s",
@@ -116,6 +141,33 @@ def generate_cover(
exc_info=True,
)
# 第 2.5 步:通过 plan_id 作为 source_edit_plan_id 查找关联的已完成预览任务
if not rendered_storage_key:
try:
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
logger.info("[封面生成] 步骤2.5: 通过 source_edit_plan_id 查找: plan_id=%s", plan_id)
preview_tasks = gen_task_repo.list_by_source_edit_plan(plan_id)
for pt in preview_tasks:
if getattr(pt, "status", "") == "completed" and getattr(pt, "is_preview", False):
video_repo = get_generated_video_repository(db)
use_case = ListGeneratedVideosByTaskUseCase(video_repo)
videos = use_case.execute(pt.id)
if videos:
rendered_storage_key = getattr(videos[0], "file_url", "") or ""
logger.info(
"[封面生成] ✅ 步骤2.5找到视频: plan_id=%s task_id=%s url=%s",
plan_id,
pt.id,
rendered_storage_key[:80],
)
break
except Exception:
logger.warning(
"封面生成: 通过 source_edit_plan_id 查找预览任务失败: plan_id=%s",
plan_id,
exc_info=True,
)
# 第三步:按 user + template 查找最近的已完成预览任务(兜底)
if not rendered_storage_key:
try:
@@ -185,28 +237,103 @@ def generate_cover(
detail=f"获取预览视频URL失败: {e}",
) from e
# 优先使用渲染时预抽的封面候选帧(跳过 MediaKit,秒级返回)
cover_candidates = (plan.config or {}).get("cover_candidates", [])
if cover_candidates and body.cover_type in ("ai_frame", "ai_regenerate"):
logger.info(
"[封面生成] 使用预存封面候选帧: plan_id=%s count=%d",
plan_id,
len(cover_candidates),
)
first_frame = cover_candidates[0]
cover_data = {
"type": "ai_frame",
"image_url": first_frame.get("image_url", ""),
"frame_time": first_frame.get("frame_time", 0.0),
"confidence": 0.9,
}
if cover_data["image_url"]:
# 统一封面管道:优先从 GenerationTask.cover_url 读取渲染后视频抽帧的封面
# 多步查找 cover_url,和查找视频 URL 一样的 fallback 逻辑
if body.cover_type in ("ai_frame", "ai_regenerate"):
cover_url_from_task = None
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
# 步骤 A:通过 generation_task_id 直接查找
generation_task_id = (plan.config or {}).get("generation_task_id", "")
if generation_task_id:
try:
task = gen_task_repo.get(generation_task_id)
if task and getattr(task, "cover_url", ""):
cover_url_from_task = task.cover_url
logger.info(
"[封面生成] 统一管道封面(步骤A-direct): plan_id=%s task_id=%s url=%s",
plan_id,
generation_task_id,
cover_url_from_task[:80],
)
except Exception:
logger.warning(
"[封面生成] 步骤A读取 cover_url 失败: plan_id=%s task_id=%s",
plan_id,
generation_task_id,
exc_info=True,
)
# 步骤 B:通过 source_edit_plan_id 查找关联预览任务的 cover_url
if not cover_url_from_task:
try:
preview_tasks = gen_task_repo.list_by_source_edit_plan(plan_id)
for pt in preview_tasks:
if getattr(pt, "status", "") == "completed" and getattr(pt, "cover_url", ""):
cover_url_from_task = pt.cover_url
logger.info(
"[封面生成] 统一管道封面(步骤B-source_plan): plan_id=%s task_id=%s url=%s",
plan_id,
pt.id,
cover_url_from_task[:80],
)
break
except Exception:
logger.warning(
"[封面生成] 步骤B查找 cover_url 失败: plan_id=%s",
plan_id,
exc_info=True,
)
# 步骤 C:通过 user+template 查找最近的已完成预览任务的 cover_url
if not cover_url_from_task:
try:
preview_tasks = gen_task_repo.list_latest_completed_preview(
user_id=str(current_user.user.id),
template_id=template_id,
)
for pt in preview_tasks:
if getattr(pt, "cover_url", ""):
cover_url_from_task = pt.cover_url
logger.info(
"[封面生成] 统一管道封面(步骤C-user+template): plan_id=%s task_id=%s url=%s",
plan_id,
pt.id,
cover_url_from_task[:80],
)
break
except Exception:
logger.warning(
"[封面生成] 步骤C查找 cover_url 失败: plan_id=%s template_id=%s",
plan_id,
template_id,
exc_info=True,
)
if cover_url_from_task:
# 标题已在预览视频渲染时烧录(ASS字幕),封面帧自然包含标题
cover_data = {
"type": "ai_frame",
"image_url": cover_url_from_task,
"frame_time": 0.0,
"confidence": 0.95,
}
current_config = dict(plan.config) if plan.config else {}
current_config["cover"] = cover_data
normalized = normalize_plan_config(current_config)
plan_svc.update_plan_config(plan_id, {"cover": normalized["cover"]})
return GenerateCoverResponse(plan_id=plan_id, cover=cover_data)
logger.warning(
"[封面生成] 统一管道未找到 cover_url: plan_id=%s",
plan_id,
)
# ai_frame/ai_regenerate 类型必须从渲染管道获取,不再回退到 AI 服务
raise HTTPException(
status_code=400,
detail="封面尚未生成,请先重新生成预览视频以触发封面自动提取",
)
from packages.shared.ai_service import run_generate_cover
try:
@@ -5,6 +5,7 @@
from __future__ import annotations
import json
import logging
from app.auth import AuthenticatedUser, get_current_user
@@ -267,6 +268,20 @@ def create_preview_generation_task(
# 从模板读取 editing_mode / mode 作为 strategy_id(渲染 pipeline 的 mode 参数)
strategy_id = _resolve_strategy_id_from_template(request.template_id, db, user_id)
# 处理标题配置:如果有标题文本,序列化到 custom_title 字段传递给 worker
title_config = request.title_config or {}
title_text = (title_config.get("text") or "").strip()
custom_title_value = ""
if title_text:
# 将标题文本和样式配置序列化为 JSON 存入 custom_title
# Worker 端会解析 JSON 获取完整标题配置
custom_title_value = json.dumps(title_config, ensure_ascii=False)
logger.info(
"[预览生成] 标题配置: text=%s, config_keys=%s",
title_text[:30],
list(title_config.keys()),
)
use_case = CreateGenerationTaskUseCase(generation_task_repository)
try:
@@ -290,6 +305,7 @@ def create_preview_generation_task(
auto_retry_enabled=False,
auto_retry_max=0,
is_preview=True,
custom_title=custom_title_value,
)
)
except ValueError as e:
@@ -299,6 +315,32 @@ def create_preview_generation_task(
logger.error("[预览生成] 创建失败: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="创建预览生成任务失败,请稍后再试") from e
# 关联编辑计划:如果前端未传 source_edit_plan_id,通过 template_id + user_id 查找
if not task.source_edit_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.edit_plan_repository import (
SQLAlchemyEditPlanRepository,
)
_plan_repo = SQLAlchemyEditPlanRepository(db)
_plans = _plan_repo.list_by_template(request.template_id, limit=20)
for _p in _plans:
if (_p.created_by_user_id or "") == user_id:
task.source_edit_plan_id = _p.id
generation_task_repository.update(task)
logger.info(
"[预览生成] 自动关联编辑计划: task_id=%s plan_id=%s",
task.id,
_p.id,
)
break
except Exception:
logger.warning(
"[预览生成] 查找关联编辑计划失败(不影响主流程): task_id=%s",
task.id,
exc_info=True,
)
# 入队执行;若入队失败则标记任务为 failed 避免僵尸数据
try:
if not safe_enqueue_generation_task(
+3 -3
View File
@@ -1,6 +1,6 @@
from datetime import datetime, timezone
import psycopg2
import psycopg
import redis
from app.config import settings
from fastapi import APIRouter, status
@@ -49,7 +49,7 @@ async def _check_database() -> dict:
"message": "Using in-memory database",
}
try:
conn = psycopg2.connect(settings.DATABASE_URL, connect_timeout=3)
conn = psycopg.connect(settings.DATABASE_URL, connect_timeout=3)
with conn.cursor() as cur:
cur.execute("SELECT 1")
cur.fetchone()
@@ -124,7 +124,7 @@ async def _check_migrations() -> dict:
"message": "Using in-memory database, no migrations needed",
}
try:
conn = psycopg2.connect(settings.DATABASE_URL, connect_timeout=3)
conn = psycopg.connect(settings.DATABASE_URL, connect_timeout=3)
with conn.cursor() as cur:
cur.execute("""
SELECT COUNT(*) FROM information_schema.tables
+110 -35
View File
@@ -16,13 +16,16 @@
from __future__ import annotations
import logging
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import get_storage_service
from app.dependencies import get_asset_repository
from app.services.edit_plan_service import EditPlanService
from app.services.edit_template_service import EditTemplateService
from fastapi import APIRouter, Depends, HTTPException, Query, status
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
from .dependencies import get_draft_plan_id, get_editor_services
from .schemas import (
ClipBatchDeleteRequest,
@@ -43,30 +46,100 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["Template Editor"])
def _clip_to_response(clip) -> EditorClipResponse:
"""统一构造片段响应"""
def _clip_to_response(clip, asset_url: str | None = None) -> EditorClipResponse:
"""统一构造片段响应 — 与 edit_plan_clips 表字段完全对齐"""
def _enum_str(val) -> str:
return val.value if hasattr(val, "value") else str(val)
def _fmt_dt(val) -> str:
if val is None:
return ""
if hasattr(val, "isoformat"):
return val.isoformat()
return str(val)
return EditorClipResponse(
id=clip.id,
plan_id=clip.plan_id,
clip_type=clip.clip_type.value
if hasattr(clip.clip_type, "value")
else str(clip.clip_type),
clip_type=_enum_str(getattr(clip, "clip_type", "")),
order=clip.order,
duration=clip.duration,
start_time=getattr(clip, "start_time", 0.0) or 0.0,
text_content=clip.text_content or "",
transition_effect=clip.transition_effect.value
if hasattr(clip.transition_effect, "value")
else str(clip.transition_effect),
transition_effect=_enum_str(getattr(clip, "transition_effect", "cut")),
transition_duration=getattr(clip, "transition_duration", 0.0) or 0.0,
playback_speed=clip.playback_speed or 1.0,
asset_id=getattr(clip, "asset_id", "") or "",
asset_url=asset_url,
status=getattr(clip, "status", "pending") or "pending",
template_clip_config_id=getattr(clip, "template_clip_config_id", "") or "",
config=clip.config or {},
created_at=_fmt_dt(getattr(clip, "created_at", None)),
updated_at=_fmt_dt(getattr(clip, "updated_at", None)),
)
def _build_asset_url_map(
asset_ids: list[str],
asset_repo: SQLAlchemyAssetRepository,
) -> dict[str, str | None]:
"""批量查询素材并生成签名URL映射.
Returns:
{asset_id: signed_url_or_None}
"""
if not asset_ids:
return {}
# 去重:多个 clip 可能引用同一个素材
# 去重并保持顺序
seen: set[str] = set()
unique_ids = []
for aid in asset_ids:
if aid and aid not in seen:
seen.add(aid)
unique_ids.append(aid)
result: dict[str, str | None] = {}
try:
storage = get_storage_service()
except Exception:
logger.warning("获取存储服务失败,跳过asset_url生成")
return {aid: None for aid in asset_ids}
# 批量查询所有 Asset(单次 SQL IN 查询,避免 N+1)
try:
assets = asset_repo.find_by_ids(unique_ids)
asset_map = {a.id: a for a in assets}
except Exception:
logger.warning("批量查询素材失败: asset_ids=%s", asset_ids, exc_info=True)
return {aid: None for aid in asset_ids if aid}
for aid in unique_ids:
try:
asset = asset_map.get(aid)
if asset is None:
result[aid] = None
continue
storage_key = getattr(asset, "storage_key", None) or ""
if not storage_key:
result[aid] = None
continue
result[aid] = storage.get_download_url(storage_key, expires_seconds=3600)
except Exception:
logger.warning("生成素材签名URL失败: asset_id=%s", aid, exc_info=True)
result[aid] = None
return result
@router.get("/clips", response_model=EditorClipListResponse)
def list_draft_clips(
template_id: str,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
skip: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=500),
_: AuthenticatedUser = Depends(get_current_user),
@@ -75,8 +148,17 @@ def list_draft_clips(
_, plan_svc = services
clips = plan_svc.list_clips(plan_id, skip=skip, limit=limit)
total = plan_svc.count_clips(plan_id)
# 批量解析素材签名URL
asset_ids = [getattr(c, "asset_id", "") or "" for c in clips]
asset_ids = [aid for aid in asset_ids if aid]
url_map = _build_asset_url_map(asset_ids, asset_repo)
return EditorClipListResponse(
items=[_clip_to_response(c) for c in clips],
items=[
_clip_to_response(c, asset_url=url_map.get(getattr(c, "asset_id", "") or ""))
for c in clips
],
total=total,
)
@@ -156,6 +238,7 @@ def get_draft_clip_detail(
clip_id: str,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
_: AuthenticatedUser = Depends(get_current_user),
):
"""获取草稿中的片段详情"""
@@ -165,16 +248,20 @@ def get_draft_clip_detail(
raise HTTPException(status_code=404, detail="片段不存在")
if clip.plan_id != plan_id:
raise HTTPException(status_code=404, detail="片段不存在")
return _clip_to_response(clip)
asset_id = getattr(clip, "asset_id", "") or ""
url_map = _build_asset_url_map([asset_id], asset_repo) if asset_id else {}
return _clip_to_response(clip, asset_url=url_map.get(asset_id))
@router.post("/clips/{clip_id}/split", response_model=dict[str, Any], status_code=status.HTTP_200_OK)
@router.post("/clips/{clip_id}/split", status_code=status.HTTP_200_OK)
def split_draft_clip(
template_id: str,
clip_id: str,
body: SplitClipRequest,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
_: AuthenticatedUser = Depends(get_current_user),
):
"""将一个片段从指定时间点分割为两个片段"""
@@ -190,32 +277,22 @@ def split_draft_clip(
) from exc
left = result["left_clip"]
right = result["right_clip"]
asset_ids = [getattr(left, "asset_id", "") or "", getattr(right, "asset_id", "") or ""]
asset_ids = [a for a in asset_ids if a]
url_map = _build_asset_url_map(asset_ids, asset_repo)
return {
"left_clip": {
"id": left.id,
"plan_id": left.plan_id,
"clip_type": left.clip_type,
"order": left.order,
"duration": left.duration,
"start_time": left.start_time,
},
"right_clip": {
"id": right.id,
"plan_id": right.plan_id,
"clip_type": right.clip_type,
"order": right.order,
"duration": right.duration,
"start_time": right.start_time,
},
"left_clip": _clip_to_response(left, asset_url=url_map.get(getattr(left, "asset_id", "") or "")),
"right_clip": _clip_to_response(right, asset_url=url_map.get(getattr(right, "asset_id", "") or "")),
}
@router.post("/clips/merge", response_model=dict[str, Any], status_code=status.HTTP_200_OK)
@router.post("/clips/merge", status_code=status.HTTP_200_OK)
def merge_draft_clips(
template_id: str,
body: MergeClipsRequest,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
_: AuthenticatedUser = Depends(get_current_user),
):
"""将多个连续的同类型片段合并为一个片段"""
@@ -230,13 +307,11 @@ def merge_draft_clips(
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)
) from exc
asset_id = getattr(merged, "asset_id", "") or ""
url_map = _build_asset_url_map([asset_id], asset_repo) if asset_id else {}
return {
"id": merged.id,
"plan_id": merged.plan_id,
"clip_type": merged.clip_type,
"order": merged.order,
"duration": merged.duration,
"text_content": merged.text_content,
"merged_clip": _clip_to_response(merged, asset_url=url_map.get(asset_id)),
"deleted_clip_ids": body.clip_ids,
}
@@ -8,8 +8,9 @@
from __future__ import annotations
import json
import logging
from typing import Any
from typing import Any, Optional
from app.auth import AuthenticatedUser, get_current_user
from app.core.celery_app import celery_app
@@ -45,6 +46,7 @@ from ._fallback import (
from .dependencies import _check_queue_limits, get_draft_plan_id, get_editor_services
from .schemas import (
ClipStatusItem,
EditPlanGenerateRequest,
EditPlanGenerateResponse,
EditPlanGenerationsResponse,
EditPlanGenerationStatusResponse,
@@ -57,6 +59,7 @@ router = APIRouter(tags=["Template Editor"])
@router.post("/generate", response_model=EditPlanGenerateResponse)
def generate_editor_draft(
template_id: str,
request: Optional[EditPlanGenerateRequest] = None,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
db: Session = Depends(get_db_session),
@@ -65,6 +68,7 @@ def generate_editor_draft(
asset_repo: Any = Depends(get_asset_repository),
) -> EditPlanGenerateResponse:
"""触发模板草稿渲染生成"""
req = request or EditPlanGenerateRequest()
_, plan_svc = services
plan_check = plan_svc.get_plan_or_raise(plan_id)
@@ -85,6 +89,36 @@ def generate_editor_draft(
# 检查是否可复用已完成的预览产物(预览品质已与正式一致)
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
reusable_task = _find_reusable_preview_task(gen_task_repo, plan_id, plan_check)
if reusable_task:
# 复用预览产物:标记为正式产出,跳过渲染
# 如果前端传了 title_config,需要创建新任务(因为预览任务的 custom_title 可能不同)
title_config_reuse = req.title_config or {}
title_text_reuse = (title_config_reuse.get("text") or "").strip()
existing_custom_title = getattr(reusable_task, "custom_title", "") or ""
if title_text_reuse and existing_custom_title:
# 如果新标题和已有标题不同,不能复用,走新建任务流程
new_title_json = json.dumps(title_config_reuse, ensure_ascii=False)
if new_title_json != existing_custom_title:
logger.info(
"[模板生成] 标题已变更,跳过复用: task_id=%s",
reusable_task.id,
)
reusable_task = None
elif title_text_reuse and not existing_custom_title:
# 原来没标题,现在有标题,不能复用
logger.info(
"[模板生成] 新增标题,跳过复用: task_id=%s",
reusable_task.id,
)
reusable_task = None
elif not title_text_reuse and existing_custom_title:
# 原来有标题,现在移除了,不能复用
logger.info(
"[模板生成] 移除标题,跳过复用: task_id=%s",
reusable_task.id,
)
reusable_task = None
if reusable_task:
# 复用预览产物:标记为正式产出,跳过渲染
reusable_task.mark_confirmed()
@@ -133,6 +167,21 @@ def generate_editor_draft(
gen_task_use_case = CreateGenerationTaskUseCase(gen_task_repo)
plan = plan_svc.get_plan_or_raise(plan_id)
config_asset_ids = (plan.config or {}).get("asset_ids", [])
# 从 plan config 读取封面 URL(由 generate-cover 保存)
cover_url_from_config = (plan.config or {}).get("cover", {}).get("image_url", "")
# 处理标题配置:序列化 title_config 为 JSON 存入 custom_title
title_config = req.title_config or {}
title_text = (title_config.get("text") or "").strip()
custom_title_value = ""
if title_text:
custom_title_value = json.dumps(title_config, ensure_ascii=False)
logger.info(
"[模板生成] 标题配置: text=%s, config_keys=%s",
title_text[:30],
list(title_config.keys()),
)
gen_task = gen_task_use_case.execute(
CreateGenerationTaskCommand(
project_id=plan.project_id or "",
@@ -140,6 +189,8 @@ def generate_editor_draft(
created_by_user_id=current_user.user.id,
source_edit_plan_id=plan_id,
asset_ids=list(config_asset_ids) if config_asset_ids else [],
cover_url=cover_url_from_config,
custom_title=custom_title_value,
),
)
@@ -6,7 +6,7 @@
from __future__ import annotations
import re as _re
from typing import Any, List, Optional
from typing import Any, Dict, List, Optional
from app.schemas.generation_task import GenerationTaskResponse
from pydantic import BaseModel, Field, validator
@@ -44,6 +44,14 @@ class EditPlanGenerationStatusResponse(BaseModel):
clips: List[ClipStatusItem]
class EditPlanGenerateRequest(BaseModel):
"""模板编辑器触发生成请求体"""
title_config: Optional[Dict[str, Any]] = Field(
default_factory=dict,
description="标题配置(可选),渲染时烧录到视频中。支持字段: text/font/font_size/font_color/position/bold/stroke/shadow",
)
class EditPlanGenerateResponse(BaseModel):
"""剪辑计划触发生成响应体"""
@@ -227,6 +235,7 @@ class ClipsFromAssetsResponse(BaseModel):
success: bool = True
created_count: int
plan_id: str = ""
message: str = ""
clip_ids: List[str] = Field(default_factory=list, description="创建的片段ID列表")
@@ -439,17 +448,28 @@ class EditorUpdateRequest(BaseModel):
class EditorClipResponse(BaseModel):
"""片段响应"""
"""片段响应 — 与数据库 edit_plan_clips 表字段对齐"""
id: str
plan_id: str
clip_type: str
order: int
duration: float
start_time: float = 0.0
text_content: str = ""
transition_effect: str = "cut"
transition_duration: float = 0.0
playback_speed: float = 1.0
asset_id: str = ""
asset_url: str | None = Field(
default=None,
description="素材视频签名URL(1小时有效),用于前端预览播放",
)
status: str = "pending"
template_clip_config_id: str = ""
config: dict[str, Any] = Field(default_factory=dict)
created_at: str = ""
updated_at: str = ""
class EditorClipListResponse(BaseModel):
+2 -1
View File
@@ -206,6 +206,7 @@ async def complete_direct_upload(
ingest_job_id="",
duplicated=True,
asset_id=existing.id,
url=storage_service.get_url(normalized_key),
)
job = _submit_ingest_job(
@@ -215,7 +216,7 @@ async def complete_direct_upload(
ingest_job_repository=ingest_job_repository,
file_hash=request.file_hash,
)
return DirectUploadCompleteResponse(storage_key=normalized_key, ingest_job_id=job.id)
return DirectUploadCompleteResponse(storage_key=normalized_key, ingest_job_id=job.id, url=storage_service.get_url(normalized_key))
@router.post(
+6
View File
@@ -58,6 +58,12 @@ class AssetResponse(BaseModel):
MAX_BATCH_SIZE = 200
class BatchGetRequest(BaseModel):
"""批量获取素材详情请求。"""
ids: list[str] = Field(..., min_length=1, max_length=MAX_BATCH_SIZE, description="素材 ID 列表")
class BatchDeleteRequest(BaseModel):
"""批量删除请求(软删除)。"""
+6 -2
View File
@@ -7,8 +7,8 @@ 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="输出视频高度")
output_width: int = Field(default=1080, ge=100, description="输出视频宽度")
output_height: int = Field(default=1920, ge=100, description="输出视频高度")
cover_url: str = Field(default="", description="自定义封面图片 URL")
custom_title: str = Field(default="", description="自定义视频标题")
@@ -181,6 +181,10 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
default="",
description="关联的编辑计划ID(可选),用于确认生成时复用预览产物",
)
title_config: dict = Field(
default_factory=dict,
description="标题配置(可选),渲染时烧录到预览视频中。支持字段: text/font/font_size/font_color/position/bold/stroke/shadow",
)
@model_validator(mode="after")
def _check_template_id(self) -> "CreatePreviewGenerationTaskRequest":
+1
View File
@@ -39,6 +39,7 @@ class DirectUploadCompleteResponse(BaseModel):
ingest_job_id: str
duplicated: bool = Field(default=False, description="是否为重复素材(命中去重)")
asset_id: str = Field(default="", description="重复素材的 asset_idduplicated=true 时返回)")
url: str = Field(default="", description="Public URL of uploaded file")
class UploadAssetResponse(BaseModel):
+11 -12
View File
@@ -200,15 +200,7 @@ test.describe("Core generation flow", () => {
await expect(page.getByRole("heading", { name: /选择配音/ })).toBeVisible({ timeout: 15000 })
await page.getByRole("button", { name: "下一步" }).click()
// 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: 300_000 })
await page.getByRole("button", { name: "下一步" }).click()
// Step 5: title
// Step 4: title(新顺序:标题在预览之前)
await expect(page.getByRole("heading", { name: /选择标题/ })).toBeVisible({ timeout: 15000 })
// 等待组件完全渲染
await page.waitForTimeout(2000)
@@ -222,6 +214,10 @@ test.describe("Core generation flow", () => {
await titleInput.fill(titleText)
await page.getByRole("button", { name: "下一步" }).click()
// Step 5: preview — 前端实时预览架构改造,无需后端生成预览
await expect(page.getByRole("heading", { name: /预览设置/ })).toBeVisible({ timeout: 15000 })
await page.getByRole("button", { name: "下一步" }).click()
// Step 6: cover (默认 AI 智能选帧模式,直接下一步)
await expect(page.getByRole("heading", { name: /选择封面/ })).toBeVisible({ timeout: 15000 })
await page.getByRole("button", { name: "下一步" }).click()
@@ -258,9 +254,12 @@ test.describe("Core generation flow", () => {
// Generate API may return 400 in test env if template has no ready segments
// That is OK for a wizard flow smoke test
if (genResp.ok()) {
const genData = (await genResp.json()) as { plan_id: string; generation_task_id: string }
expect(genData.plan_id).toBeTruthy()
expect(genData.generation_task_id).toBeTruthy()
const genData = (await genResp.json()) as {
items: Array<{ id: string; status: string }>
total: number
}
expect(genData.items.length).toBeGreaterThan(0)
expect(genData.items[0].id).toBeTruthy()
} else {
console.log(`[E2E] Generate API returned ${genResp.status()}, wizard flow test still passes`)
}
+31 -39
View File
@@ -178,7 +178,7 @@ test.describe("素材库流程", () => {
expect(kinds).toContain("image")
})
test("创建素材记录", async ({ request }) => {
test("创建素材记录 — POST /assets 已废弃返回 410", async ({ request }) => {
const { headers, userId } = await createAuthedUser(request, "asset-create")
const projectId = await createProject(request, headers, Date.now().toString())
@@ -194,7 +194,7 @@ test.describe("素材库流程", () => {
expect(lib.ok()).toBeTruthy()
const libData = await lib.json()
// 创建素材记录
// POST /assets 已废弃,应返回 410 Gone
const response = await request.post(`${apiBase}/assets`, {
headers,
data: {
@@ -210,16 +210,9 @@ test.describe("素材库流程", () => {
},
})
expect(
response.ok(),
`创建素材应返回 2xx,实际: ${response.status()} ${await response.text()}`,
).toBeTruthy()
expect(response.status()).toBe(410)
const data = await response.json()
expect(data.id, "应返回素材 ID").toBeTruthy()
expect(data.name).toContain("test_video")
expect(data.mime_type).toBe("video/mp4")
expect(data.library_id).toBe(libData.id)
expect(data.error?.code).toBe("HTTP_410")
})
test("列出素材", async ({ request }) => {
@@ -232,51 +225,50 @@ test.describe("素材库流程", () => {
data: {
project_id: projectId,
name: `List Lib ${Date.now()}`,
kind: "video",
kind: "image",
},
})
expect(lib.ok(), `创建素材库应成功: ${await lib.text()}`).toBeTruthy()
const libData = await lib.json()
// 创建 2 个素材
await request.post(`${apiBase}/assets`, {
// 通过 multipart upload 上传 2 个小图片作为测试素材
// 创建一个 1x1 的 PNG buffer
const tinyPng = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"base64",
)
await request.post(`${apiBase}/upload`, {
headers,
data: {
multipart: {
project_id: projectId,
library_id: libData.id,
name: `clip_a_${Date.now()}.mp4`,
storage_key: `uploads/e2e/clip_a.mp4`,
mime_type: "video/mp4",
status: "ready",
uploaded_by_user_id: userId,
file: { name: "clip_a.png", mimeType: "image/png", buffer: tinyPng },
},
})
await request.post(`${apiBase}/assets`, {
await request.post(`${apiBase}/upload`, {
headers,
data: {
multipart: {
project_id: projectId,
library_id: libData.id,
name: `clip_b_${Date.now()}.mp4`,
storage_key: `uploads/e2e/clip_b.mp4`,
mime_type: "video/mp4",
status: "ready",
uploaded_by_user_id: userId,
file: { name: "clip_b.png", mimeType: "image/png", buffer: tinyPng },
},
})
// 列出素材
const response = await request.get(`${apiBase}/assets`, {
headers,
params: { library_id: libData.id },
})
// 列出素材(可能需要等待 ingest job 完成)
let items: any[] = []
for (let i = 0; i < 10; i++) {
const response = await request.get(`${apiBase}/assets`, {
headers,
params: { library_id: libData.id },
})
expect(response.ok(), `列出素材应返回 2xx`).toBeTruthy()
const data = await response.json()
items = data.items || []
if (items.length >= 2) break
await new Promise((r) => setTimeout(r, 2000))
}
expect(
response.ok(),
`列出素材应返回 2xx,实际: ${response.status()} ${await response.text()}`,
).toBeTruthy()
const data = await response.json()
const items = data.items || []
expect(items.length, "应至少有 2 个素材").toBeGreaterThanOrEqual(2)
})
+10
View File
@@ -12,6 +12,7 @@
"@tanstack/react-query": "^5.45.0",
"antd": "^5.18.0",
"axios": "^1.7.2",
"mp4box": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
@@ -4623,6 +4624,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/mp4box": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/mp4box/-/mp4box-2.4.1.tgz",
"integrity": "sha512-0HGX7nXoDIX6FKLVl4a3wtYjBlwqsN3xuQC3GXzNtKp98FXUOhDSq623azsz8DG5ptd9ZXcXodDkgbdMZOjWvw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=20.8.1"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+1
View File
@@ -23,6 +23,7 @@
"@tanstack/react-query": "^5.45.0",
"antd": "^5.18.0",
"axios": "^1.7.2",
"mp4box": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.24.0",
-12
View File
@@ -60,18 +60,6 @@ export const smartMatchAssets = async (libraryId: string): Promise<{ items: Asse
return response.data
}
/** 创建素材(上传文件后调用,附带 metadata) */
export const createAsset = async (data: {
library_id: string
name: string
storage_key: string
mime_type: string
metadata?: AssetMetadata
}): Promise<AssetItem> => {
const response = await apiClient.post("/assets", data)
return response.data
}
/** 更新素材(名称、metadata 等) */
export const updateAsset = async (
assetId: string,
+1 -3
View File
@@ -13,7 +13,6 @@ export type {
ClassificationJob,
AssetDiagnosis,
BatchOperationResult,
UploadResult,
DirectUploadPrepareResult,
DirectUploadCompleteResult,
} from "./types"
@@ -34,14 +33,13 @@ export {
getAssets,
getAssetsByKind,
smartMatchAssets,
createAsset,
updateAsset,
updateAssetReviewStatus,
deleteAsset,
} from "./assets"
// 上传
export { uploadAsset, prepareDirectUpload, completeDirectUpload, uploadAssetDirect } from "./upload"
export { prepareDirectUpload, completeDirectUpload, uploadAssetDirect } from "./upload"
// 任务
export { getIngestJob, submitClassificationJob, getClassificationJob } from "./jobs"
+1
View File
@@ -135,4 +135,5 @@ export interface DirectUploadPrepareResult {
export interface DirectUploadCompleteResult {
storage_key: string
ingest_job_id: string
url: string
}
+1 -10
View File
@@ -3,16 +3,7 @@
*/
import apiClient from "../client"
import { getOrCreateDefaultProject } from "../projects"
import type { UploadResult, DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types"
/** 表单上传素材(小文件) */
export const uploadAsset = async (formData: FormData): Promise<UploadResult> => {
const response = await apiClient.post("/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
timeout: 30 * 60 * 1000,
})
return response.data
}
import type { DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types"
/** 预签名直传准备 */
export const prepareDirectUpload = async (data: {
+109
View File
@@ -0,0 +1,109 @@
/**
* 主动 Token 刷新模块
*
* 在 access_token 过期前主动刷新,避免 API 请求触发 401。
* JWT payload 是 base64 编码的 JSON,无需第三方库即可解码。
*/
import { useAuthStore } from "@/store/authStore"
import { refreshAccessToken } from "./login"
let refreshTimer: ReturnType<typeof setTimeout> | null = null
/** 正在执行刷新操作的 Promise,防止主动刷新和 401 被动刷新并发竞争 */
let activeRefreshPromise: Promise<void> | null = null
/** 提前刷新的缓冲时间(秒) */
const REFRESH_BUFFER_SECONDS = 60
/**
* 解码 JWT payload(不验签,仅读取 exp 字段)
*/
function decodeJwtPayload(token: string): { exp?: number } | null {
try {
const parts = token.split(".")
if (parts.length !== 3) return null
// JWT 使用 base64url 编码,需要转换为标准 base64
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/")
const padded = payload + "=".repeat((4 - (payload.length % 4)) % 4)
const decoded = atob(padded)
return JSON.parse(decoded)
} catch {
return null
}
}
/**
* 取消已调度的主动刷新
*/
export function cancelProactiveRefresh(): void {
if (refreshTimer) {
clearTimeout(refreshTimer)
refreshTimer = null
}
}
/**
* 执行 token 刷新(带并发锁,供主动刷新和被动 401 共用)
* 返回当前刷新操作的 Promise;若已有刷新进行中则复用该 Promise。
*/
export function executeTokenRefresh(): Promise<void> | null {
// 已有刷新进行中 → 复用
if (activeRefreshPromise) {
return activeRefreshPromise
}
const { user, refreshToken: refreshTokenValue } = useAuthStore.getState()
// 安全检查:user 或 refreshToken 为空时跳过刷新
if (!user || !refreshTokenValue) {
return null
}
activeRefreshPromise = (async () => {
try {
const data = await refreshAccessToken(refreshTokenValue)
const newAccessToken = data.access_token
const newRefreshToken = data.refresh_token ?? refreshTokenValue
// 更新 Zustand store + localStorage
useAuthStore.getState().setAuth(user, newAccessToken, newRefreshToken)
// 递归调度下一次刷新
scheduleProactiveRefresh()
} catch {
// 刷新失败 → 清除认证状态,跳转登录页
cancelProactiveRefresh()
useAuthStore.getState().clearAuth()
window.location.href = "/login"
} finally {
activeRefreshPromise = null
}
})()
return activeRefreshPromise
}
/**
* 调度主动刷新:在 token 过期前 REFRESH_BUFFER_SECONDS 秒自动刷新
*/
export function scheduleProactiveRefresh(): void {
cancelProactiveRefresh()
// 统一从 Zustand store 读取(与 setAuth 写入保持一致)
const { accessToken, refreshToken: refreshTokenValue } = useAuthStore.getState()
if (!accessToken || !refreshTokenValue) return
const payload = decodeJwtPayload(accessToken)
if (!payload?.exp) return
const now = Math.floor(Date.now() / 1000)
const secondsUntilExpiry = payload.exp - now
// 如果 token 已经过期或即将在缓冲时间内过期,立即刷新
const delaySeconds = Math.max(secondsUntilExpiry - REFRESH_BUFFER_SECONDS, 0)
refreshTimer = setTimeout(() => {
executeTokenRefresh()
}, delaySeconds * 1000)
}
+18 -8
View File
@@ -5,7 +5,8 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios"
import { message } from "antd"
import { useAuthStore } from "@/store/authStore"
import { refreshAccessToken } from "./auth"
import { cancelProactiveRefresh, executeTokenRefresh } from "./auth/tokenRefresh"
// 创建 Axios 实例
const apiClient = axios.create({
@@ -97,14 +98,22 @@ apiClient.interceptors.response.use(
isRefreshing = true
try {
const data = await refreshAccessToken(refreshToken)
const newAccessToken = data.access_token
const newRefreshToken = data.refresh_token ?? refreshToken
// 使用共享的刷新函数(带并发锁 + 安全检查)
const refreshPromise = executeTokenRefresh()
if (!refreshPromise) {
// user 或 refreshToken 为空,无法刷新
cancelProactiveRefresh()
useAuthStore.getState().clearAuth()
window.location.href = "/"
return Promise.reject(new Error("Unable to refresh: missing user or refresh token"))
}
await refreshPromise
// 更新 Zustand + localStorage
useAuthStore
.getState()
.setAuth(useAuthStore.getState().user!, newAccessToken, newRefreshToken)
// 获取刷新后的新 token
const newAccessToken = useAuthStore.getState().accessToken
if (!newAccessToken) {
return Promise.reject(new Error("Token refresh failed: no new access token"))
}
// 处理排队的请求
processQueue(null, newAccessToken)
@@ -116,6 +125,7 @@ apiClient.interceptors.response.use(
return apiClient(originalRequest)
} catch (refreshError) {
// 刷新失败 → 登出
cancelProactiveRefresh()
processQueue(refreshError, null)
useAuthStore.getState().clearAuth()
window.location.href = "/"
+4 -8
View File
@@ -23,13 +23,9 @@ export async function generateCover(
templateId: string,
data: GenerateCoverRequest,
): Promise<GenerateCoverResponse> {
const response = await apiClient.post<GenerateCoverResponse>(
"/generation/generate-cover",
{ ...data, template_id: templateId },
{
timeout: 300000,
params: { template_id: templateId },
},
)
const response = await apiClient.post<GenerateCoverResponse>("/generation/generate-cover", data, {
timeout: 300000,
params: { template_id: templateId },
})
return response.data
}
-4
View File
@@ -3,13 +3,9 @@ export type {
CreatePreviewRequest,
CreatePreviewResponse,
PreviewTaskResponse,
ConfirmGenerationRequest,
ConfirmGenerationResponse,
ConfirmGenerationTaskItem,
} from "./types"
export { createPreview, getPreviewStatus } from "./preview"
export { confirmGeneration } from "./confirm"
export { generateCover } from "./cover"
export type { GenerateCoverRequest, GenerateCoverResponse } from "./cover"
+11
View File
@@ -13,6 +13,17 @@ export interface CreatePreviewRequest {
video_title?: string
duration?: number
video_ratio?: string
/* 标题烧录配置(可选,传入后 ASS 渲染标题到预览视频中) */
title_config?: {
text?: string
font?: string
font_size?: number
font_color?: string
position?: string
bold?: boolean
stroke?: boolean
shadow?: boolean
}
bgm_config?: {
enabled: boolean
preset_id?: string
+25 -2
View File
@@ -57,8 +57,31 @@ export interface TaskListResponse {
export interface CreateGenerationTaskRequest {
template_id: string
asset_ids: string[]
title_ids: string[]
voice_ids: string[]
title_ids?: string[]
voice_ids?: string[]
/** 输出视频宽度 */
output_width?: number
/** 输出视频高度 */
output_height?: number
/** 自定义封面图片 URL */
cover_url?: string
/** 自定义视频标题 */
custom_title?: string
/** 视频时长(秒) */
duration?: number
/** 视频宽高比,如 "9:16" */
video_ratio?: string
/** 标题烧录配置 */
title_config?: {
text?: string
font?: string
font_size?: number
font_color?: string
position?: string
bold?: boolean
stroke?: boolean
shadow?: boolean
}
}
/** 创建生成任务响应(对齐后端 GenerationTaskResponse */
@@ -1,7 +1,8 @@
import React, { useState, useCallback, useRef, useEffect } from "react"
import { Modal, Button } from "@/components/ui"
import { createVoiceClone, toVoiceClone } from "@/api/voice-clone"
import { uploadAsset } from "@/api/assets"
import { uploadAssetDirect, ensureDefaultLibrary } from "@/api/assets"
import { getOrCreateDefaultProject } from "@/api/projects"
import { PROGRESS_STEPS, ACCEPTED_MIME } from "./constants"
import { validateFile } from "./utils"
import { useAudioRecorder } from "./hooks/useAudioRecorder"
@@ -181,9 +182,15 @@ const CloneModal: React.FC<CloneModalProps> = ({ open, onClose, onSuccess }) =>
})
}
const formData = new FormData()
formData.append("file", fileToUpload)
const uploadResult = await uploadAsset(formData)
// 获取默认项目和素材库
const project = await getOrCreateDefaultProject()
const library = await ensureDefaultLibrary({ project_id: project.id, kind: "voice" })
// 直传到 OSS
const uploadResult = await uploadAssetDirect({
file: fileToUpload,
library_id: library.id,
})
// 组件已卸载则中止后续操作
if (!isMountedRef.current) return
@@ -1,6 +1,6 @@
import { useRef, useCallback, useEffect } from "react"
import { createVoiceClone, toVoiceClone } from "@/api/voice-clone"
import { uploadAsset, ensureDefaultLibrary } from "@/api/assets"
import { uploadAssetDirect, ensureDefaultLibrary } from "@/api/assets"
import { getOrCreateDefaultProject } from "@/api/projects"
import type { VoiceClone } from "@/api/voice-clone"
@@ -62,15 +62,15 @@ export function useCloneSubmit({
})
}
// 获取默认项目和素材库(后端 /upload 接口必填)
// 获取默认项目和素材库
const project = await getOrCreateDefaultProject()
const library = await ensureDefaultLibrary({ project_id: project.id, kind: "voice" })
const formData = new FormData()
formData.append("file", fileToUpload)
formData.append("project_id", project.id)
formData.append("library_id", library.id)
const uploadResult = await uploadAsset(formData)
// 直传到 OSS
const uploadResult = await uploadAssetDirect({
file: fileToUpload,
library_id: library.id,
})
// 阶段 2:克隆
setPhase("cloning")
+8
View File
@@ -4,6 +4,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useNavigate } from "react-router-dom"
import * as authApi from "@/api/auth"
import { scheduleProactiveRefresh, cancelProactiveRefresh } from "@/api/auth/tokenRefresh"
import { useAuthStore } from "@/store/authStore"
// 登录 Hook
@@ -31,6 +32,9 @@ export const useLogin = () => {
const user = await authApi.getCurrentUser()
setAuth(user, data.access_token, refreshToken)
// 启动主动 token 刷新,避免后续请求触发 401
scheduleProactiveRefresh()
// 跳转到登录前页面或仪表盘(与 Login.tsx onFinish 保持一致)
const redirect = localStorage.getItem("login_redirect") || "/app/dashboard"
localStorage.removeItem("login_redirect")
@@ -73,6 +77,9 @@ export const useWechatCallback = () => {
const user = await authApi.getCurrentUser()
setAuth(user, result.access_token, result.refresh_token)
// 启动主动 token 刷新
scheduleProactiveRefresh()
return { ...result, user }
}
@@ -121,6 +128,7 @@ export const useLogout = () => {
} catch (error) {
// 即使登出失败也清除本地状态
} finally {
cancelProactiveRefresh()
clearAuth()
queryClient.clear()
navigate("/")
+7
View File
@@ -9,6 +9,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ConfigProvider, App as AntApp } from "antd"
import zhCN from "antd/locale/zh_CN"
import router from "./router"
import { scheduleProactiveRefresh } from "./api/auth/tokenRefresh"
// 应用启动时,如果用户已登录,立即调度主动 token 刷新
// 这样可以在 token 过期前自动刷新,避免 API 请求触发 401
if (localStorage.getItem("access_token")) {
scheduleProactiveRefresh()
}
import "./index.css"
import "./styles/global.css"
+79 -64
View File
@@ -1,21 +1,27 @@
/**
* 智能剪辑页面 — V22 多预览 + 配音前置
* 7 步向导:选择模板 → 选择素材 → 选择配音 → 生成预览 → 选择标题 → 选择封面 → 确认生成
* 智能剪辑页面 — V24 前端预览播放器架构改造
* 7 步向导:选择模板 → 素材 → 配音 → 标题 → 预览 → 封面 → 确认生成
* 左右布局:左侧 generate-form + 右侧 generate-preview
* 主组件仅保留整体布局与事件编排
* 状态管理 → hooks/useGenerateFormState
* 步骤导航 → hooks/useStepNavigation
* 步骤内容 → components/GenerateStepContent
* 底部按钮 → components/GenerateStepActions
* 生成核心逻辑 → hooks/useGenerateVideo
*
* 架构改造:
* - Step5 预览改为前端素材切片播放(FrontendPreviewPlayer
* - 完全去除后端 FFmpeg 预览依赖
* - 标题样式通过 CSS 层实时叠加,所见即所得
* - 最终成片仍走后端 FFmpeg 渲染(Step7 确认生成)
*/
import React, { useState, useMemo } from "react"
import React, { useMemo } from "react"
import { Modal, message } from "antd"
import { useNavigate } from "react-router-dom"
import type { VoiceClone } from "@/api/voice-clone"
import { useQuery } from "@tanstack/react-query"
import { useCloneProgress } from "@/hooks/useCloneProgress"
import { getAssetsByKind } from "@/api/assets"
import CloneModal from "@/components/voice/CloneModal"
import GenerateHeader from "./components/GenerateHeader"
import {
calculateTotalVideoDuration,
estimateTotalVideoDuration,
} from "./utils/calculateTotalVideoDuration"
import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateResultPanel from "./components/GenerateResultPanel"
import PreviewVideoPanel from "./components/PreviewVideoPanel"
@@ -24,7 +30,8 @@ import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState"
import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { useStep4Preview } from "./hooks/useStep4Preview"
import { usePreviewAssets } from "./hooks/usePreviewAssets"
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters"
import "./generate.css"
const GeneratePage: React.FC = () => {
@@ -71,6 +78,12 @@ const GeneratePage: React.FC = () => {
setPreviewModalOpen,
} = formState
/* ── 标题样式回调(Step5 样式面板 + 右侧预览 CSS 层共用) ── */
const styleUpdaters = useTitleStyleUpdaters({
titleSettings,
onTitleSettingsChange: setTitleSettings,
})
/* ── 克隆声音 ── */
const { clones: clonedVoices, addClone, hasProcessing } = useCloneProgress()
@@ -80,36 +93,44 @@ const GeneratePage: React.FC = () => {
message.success("音色克隆成功!")
}
/* ── 预览数量(多预览) ── */
const [previewCount, setPreviewCount] = useState(1)
/* ── 前端预览:加载选中素材的视频文件信息 ── */
const previewAssetIds = useMemo(
() => (materialMode === "auto" ? smartSelectedIds : selectedMaterials),
[materialMode, smartSelectedIds, selectedMaterials],
)
const previewAssetsEnabled = currentStep >= 4 && previewAssetIds.length > 0
const {
assets: previewAssets,
loading: previewAssetsLoading,
ready: previewAssetsReady,
} = usePreviewAssets(previewAssetIds, previewAssetsEnabled)
/* ── 根据 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])
/* ── 当前模板对象(传给前端预览播放器) ── */
const currentTemplate = useMemo(
() => userTemplates.find((t) => t.id === selectedTemplate) || null,
[userTemplates, selectedTemplate],
)
/* ── Step4 预览生成(多预览 + voice_ids ── */
const step4Preview = useStep4Preview({
templates: userTemplates,
selectedTemplate,
materialMode,
selectedMaterials,
smartSelectedIds,
duration,
videoRatio,
voiceIds: previewVoiceIds,
voiceLibraryId: selectedVoice || undefined,
previewCount,
/* ── 视频总时长计算(用于配音时长校验 ── */
const totalVideoDuration = useMemo(() => {
// 优先用素材精确时长;素材未加载时用模板 segments 的 duration_max 之和估算
const exact = calculateTotalVideoDuration(previewAssets, currentTemplate ?? undefined)
if (exact > 0) return exact
return estimateTotalVideoDuration(currentTemplate ?? undefined)
}, [previewAssets, currentTemplate])
/* ── 配音音频 URL ── */
const { data: voiceMaterials = [] } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
const voiceAudioUrl = useMemo(() => {
if (!selectedVoice) return undefined
const asset = voiceMaterials.find((v) => v.id === selectedVoice)
return asset?.file_url || undefined
}, [selectedVoice, voiceMaterials])
/* ── 步骤导航 ── */
const { goNext, goPrev } = useStepNavigation({
currentStep,
@@ -119,7 +140,7 @@ const GeneratePage: React.FC = () => {
selectedMaterials,
smartSelectedIds,
titleSettings,
previewReady: step4Preview.canProceed,
previewReady: previewAssetsReady,
})
/* ── 视频生成核心逻辑 ── */
@@ -150,7 +171,6 @@ const GeneratePage: React.FC = () => {
autoSubtitles,
bgm,
generateCount,
previewTaskId: step4Preview.selectedTaskId,
})
/* ================================================================
@@ -182,11 +202,23 @@ const GeneratePage: React.FC = () => {
onSmartSelectedIdsChange={setSmartSelectedIds}
titleSettings={titleSettings}
onTitleSettingsChange={setTitleSettings}
/* 标题样式回调 */
onUpdatePosition={styleUpdaters.updatePosition}
onUpdateFont={styleUpdaters.updateFont}
onUpdateSize={styleUpdaters.updateSize}
onToggleBold={styleUpdaters.toggleBold}
onToggleItalic={styleUpdaters.toggleItalic}
onToggleStroke={styleUpdaters.toggleStroke}
onToggleShadow={styleUpdaters.toggleShadow}
onApplyPreset={styleUpdaters.applyPreset}
activePreset={styleUpdaters.activePreset}
titlePresets={styleUpdaters.titlePresets}
coverSettings={coverSettings}
onCoverSettingsChange={setCoverSettings}
duration={duration}
selectedVoice={selectedVoice}
onSelectedVoiceChange={setSelectedVoice}
totalVideoDuration={totalVideoDuration}
voiceMode={voiceMode}
onVoiceModeChange={setVoiceMode}
selectedClonedVoice={selectedClonedVoice}
@@ -206,21 +238,6 @@ const GeneratePage: React.FC = () => {
onRetry={handleRetryGenerate}
onDismissError={handleDismissError}
presetVoices={presetVoices}
videoRatio={videoRatio}
/* 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
@@ -236,22 +253,19 @@ const GeneratePage: React.FC = () => {
{/* ════ 右侧:预览 + 生成结果 ════ */}
<div className="xx-generate-right-col">
{/* 预览视频面板(Step4+ 常驻,展示选中的预览) */}
{/* 预览视频面板(Step4+ 显示,含 CSS 标题实时预览 */}
{currentStep >= 4 && (
<PreviewVideoPanel
previewStatus={step4Preview.previewStatus}
previewResult={step4Preview.previewResult}
previewError={step4Preview.previewError}
progress={step4Preview.progress}
assets={previewAssets}
template={currentTemplate}
videoRatio={videoRatio}
onRegenerate={step4Preview.regeneratePreview}
titleText={titleSettings.title}
titleSettings={currentStep >= 5 ? titleSettings : undefined}
assetsReady={previewAssetsReady}
assetsLoading={previewAssetsLoading}
titleSettings={titleSettings}
voiceAudioUrl={voiceAudioUrl}
/>
)}
{/* 正式生成结果(Step5+ 才显示) */}
{currentStep >= 5 && (
{currentStep >= 6 && (
<GenerateResultPanel
generated={generated}
generating={generating}
@@ -272,6 +286,7 @@ const GeneratePage: React.FC = () => {
{/* ── 视频预览弹窗 ── */}
<Modal
className="xx-preview-modal"
open={previewModalOpen}
onCancel={() => setPreviewModalOpen(false)}
footer={null}
@@ -0,0 +1,514 @@
/**
* 前端预览播放器 — Canvas + WebCodecs 方案
*
* 架构:
* - 浏览器支持 WebCodecs → Canvas 渲染(帧级精确控制 + 标题合成)
* - 浏览器不支持 → fallback 到多 video 元素方案
*
* 对外 API 不变:assets, template, videoRatio, ready, voiceAudioUrl
*/
import React, { useMemo, useCallback, useState, useRef, useEffect } from "react"
import {
PlayCircleOutlined,
PauseCircleOutlined,
SoundOutlined,
LoadingOutlined,
} from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler"
import { useCanvasPlayer, isWebCodecsSupported } from "../hooks/useCanvasPlayer"
interface FrontendPreviewPlayerProps {
assets: AssetItem[]
template: EditingTemplate | null
videoRatio: string
ready: boolean
voiceAudioUrl?: string
titleSettings?: {
title: string
size: number
font: string
color: string
position: "top" | "center" | "bottom"
bold?: boolean
italic?: boolean
stroke?: boolean
shadow?: boolean
}
}
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${m}:${s.toString().padStart(2, "0")}`
}
/**
* 将素材映射为播放片段(复用原逻辑)
*/
function buildPlaybackSegments(
assets: AssetItem[],
template: EditingTemplate | null,
): PlaybackSegment[] {
if (!assets.length) return []
const templateSegments = template?.segments || []
const segments: PlaybackSegment[] = []
assets.forEach((asset, i) => {
const assetDuration = asset.duration || asset.metadata?.duration || 30
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
const segDuration = tplSeg
? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration))
: Math.min(assetDuration, 10)
const startTime = 0
const endTime = Math.min(startTime + segDuration, assetDuration)
const videoUrl = asset.file_url || asset.storage_key
segments.push({ assetId: asset.id, videoUrl, startTime, endTime, order: i })
})
return segments
}
const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
assets,
template,
videoRatio: _videoRatio,
ready,
voiceAudioUrl,
titleSettings,
}) => {
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
const useWebCodecs = isWebCodecsSupported()
// ── 两条路径共用同一个 canvas ref(fallback 路径不使用) ──
const canvasRef = useRef<HTMLCanvasElement>(null)
// ── Canvas 播放器(WebCodecs 路径) ──
const canvasTitle = titleSettings
? {
text: titleSettings.title || "标题预览",
fontSize: titleSettings.size,
fontFamily: titleSettings.font || "思源黑体",
color: titleSettings.color || "#ffffff",
position: titleSettings.position || "bottom",
bold: titleSettings.bold,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
}
: undefined
const canvasSegments = useMemo(
() =>
segments.map((s) => ({
assetId: s.assetId,
videoUrl: s.videoUrl,
startTime: s.startTime,
endTime: s.endTime,
})),
[segments],
)
const { state: canvasState, controls: canvasControls } = useCanvasPlayer(
canvasRef,
canvasSegments,
useWebCodecs ? canvasTitle : undefined,
)
// ── Video 播放器(fallback 路径) ──
const {
isPlaying: videoIsPlaying,
currentTime: videoCurrentTime,
totalDuration: videoTotalDuration,
currentSegmentIndex: videoCurrentSegIdx,
canPlay: videoCanPlay,
togglePlayPause: videoTogglePlayPause,
seekTo: videoSeekTo,
videoRefs,
} = useSegmentScheduler(segments)
// 选择哪条路径的状态
const isPlaying = useWebCodecs ? canvasState.isPlaying : videoIsPlaying
const currentTime = useWebCodecs ? canvasState.currentTime : videoCurrentTime
const totalDuration = useWebCodecs ? canvasState.duration : videoTotalDuration
const canPlay = useWebCodecs ? canvasState.isReady : videoCanPlay
const isBuffering = useWebCodecs ? canvasState.isBuffering : false
// ── 配音音频同步 ──
const audioRef = useRef<HTMLAudioElement | null>(null)
const prevIsPlayingRef = useRef(false)
useEffect(() => {
if (!voiceAudioUrl) {
if (audioRef.current) {
audioRef.current.pause()
audioRef.current.src = ""
audioRef.current = null
}
return
}
if (!audioRef.current) {
audioRef.current = new Audio()
audioRef.current.preload = "auto"
}
if (audioRef.current.src !== voiceAudioUrl) {
audioRef.current.src = voiceAudioUrl
}
}, [voiceAudioUrl])
useEffect(() => {
const audio = audioRef.current
if (!audio || !audio.src) return
if (isPlaying && !prevIsPlayingRef.current) {
audio.currentTime = currentTime
audio.play().catch(() => {})
} else if (!isPlaying && prevIsPlayingRef.current) {
audio.pause()
}
prevIsPlayingRef.current = isPlaying
}, [isPlaying, currentTime])
// 片段切换时同步音频(仅 fallback 路径需要)
const segmentSyncKey = useWebCodecs ? -1 : videoCurrentSegIdx
useEffect(() => {
const audio = audioRef.current
if (!audio || !audio.src || !isPlaying) return
audio.currentTime = currentTime
}, [segmentSyncKey, isPlaying, currentTime])
const handleSeekTo = useCallback(
(time: number) => {
if (useWebCodecs) {
canvasControls.seek(time)
} else {
videoSeekTo(time)
}
const audio = audioRef.current
if (audio && audio.src) {
audio.currentTime = time
}
},
[useWebCodecs, canvasControls, videoSeekTo],
)
const handleTogglePlay = useCallback(() => {
if (useWebCodecs) {
if (canvasState.isPlaying) {
canvasControls.pause()
} else {
canvasControls.play()
}
} else {
videoTogglePlayPause()
}
}, [useWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause])
// ── 进度条拖拽 ──
const [isDragging, setIsDragging] = useState(false)
const progressRef = useRef<HTMLDivElement>(null)
const handleProgressClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!progressRef.current || totalDuration <= 0) return
const rect = progressRef.current.getBoundingClientRect()
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
handleSeekTo(ratio * totalDuration)
},
[totalDuration, handleSeekTo],
)
const handleMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(true)
handleProgressClick(e)
},
[handleProgressClick],
)
useEffect(() => {
if (!isDragging) return
const handleMouseMove = (e: MouseEvent) => {
if (!progressRef.current || totalDuration <= 0) return
const rect = progressRef.current.getBoundingClientRect()
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
handleSeekTo(ratio * totalDuration)
}
const handleMouseUp = () => setIsDragging(false)
window.addEventListener("mousemove", handleMouseMove)
window.addEventListener("mouseup", handleMouseUp)
return () => {
window.removeEventListener("mousemove", handleMouseMove)
window.removeEventListener("mouseup", handleMouseUp)
}
}, [isDragging, totalDuration, handleSeekTo])
const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0
// ── Canvas ResizeObserver ──
const canvasContainerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!useWebCodecs || !canPlay) return
const container = canvasContainerRef.current
const canvas = canvasRef.current
if (!container || !canvas) return
// 立即设置一次 canvas 像素分辨率,避免默认 300×150 导致首帧变形
const initRect = container.getBoundingClientRect()
if (initRect.width > 0 && initRect.height > 0) {
const dpr = window.devicePixelRatio || 1
canvas.width = initRect.width * dpr
canvas.height = initRect.height * dpr
}
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
if (width > 0 && height > 0) {
canvas.width = width * window.devicePixelRatio
canvas.height = height * window.devicePixelRatio
}
}
})
ro.observe(container)
return () => ro.disconnect()
}, [useWebCodecs, canPlay])
// ── 未就绪 ──
if (!ready || !assets.length) {
return (
<div
className="xx-preview-empty"
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
}}
>
<SoundOutlined style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }} />
<p className="xx-preview-empty-title">...</p>
<p className="xx-preview-empty-desc"></p>
</div>
)
}
// ── 无播放片段 ──
if (!canPlay) {
return (
<div
className="xx-preview-empty"
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
zIndex: 1,
}}
>
{isBuffering ? (
<>
<LoadingOutlined style={{ fontSize: 48, color: "#fff", marginBottom: 12 }} spin />
<p style={{ color: "rgba(255,255,255,0.8)" }}>...</p>
</>
) : (
<>
<PlayCircleOutlined
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
/>
<p className="xx-preview-empty-title"></p>
<p className="xx-preview-empty-desc"></p>
</>
)}
</div>
)
}
return (
<>
{/* ── Canvas 渲染层(WebCodecs 路径) ── */}
{useWebCodecs && (
<div
ref={canvasContainerRef}
style={{
position: "absolute",
inset: 0,
zIndex: 1,
background: "#000",
}}
>
<canvas
ref={canvasRef}
style={{
width: "100%",
height: "100%",
objectFit: "contain",
}}
/>
</div>
)}
{/* ── Video 渲染层(fallback 路径) ── */}
{!useWebCodecs &&
segments.map((seg, i) => (
<video
key={seg.assetId}
muted
ref={(el) => {
videoRefs.current[i] = el
}}
preload={
i === videoCurrentSegIdx ? "auto" : i === videoCurrentSegIdx + 1 ? "metadata" : "none"
}
src={seg.videoUrl}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "contain",
background: "#000",
zIndex: 1,
opacity: i === videoCurrentSegIdx ? 1 : 0,
pointerEvents: i === videoCurrentSegIdx ? "auto" : "none",
}}
playsInline
/>
))}
{/* 播放按钮 */}
{!isPlaying && (
<button
className="xx-preview-play-btn"
onClick={handleTogglePlay}
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "rgba(0,0,0,0.5)",
border: "none",
borderRadius: "50%",
width: 56,
height: 56,
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#fff",
fontSize: 28,
zIndex: 10,
}}
>
<PlayCircleOutlined />
</button>
)}
{/* 片段指示器 */}
<div
style={{
position: "absolute",
top: 8,
left: 8,
background: "rgba(0,0,0,0.6)",
color: "#fff",
fontSize: 11,
padding: "2px 8px",
borderRadius: 4,
zIndex: 10,
}}
>
{useWebCodecs ? "Canvas" : `片段 ${videoCurrentSegIdx + 1}/${segments.length}`}
</div>
{/* 控制条 */}
<div
className="xx-preview-controls"
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
display: "flex",
alignItems: "center",
gap: 8,
padding: "8px 12px",
background: "linear-gradient(transparent, rgba(0,0,0,0.6))",
zIndex: 10,
}}
>
<button
onClick={handleTogglePlay}
style={{
background: "none",
border: "none",
color: "#fff",
fontSize: 18,
cursor: "pointer",
padding: 4,
display: "flex",
alignItems: "center",
}}
>
{isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
</button>
<span
style={{
fontSize: 12,
color: "rgba(255,255,255,0.8)",
minWidth: 80,
fontVariantNumeric: "tabular-nums",
}}
>
{formatTime(currentTime)} / {formatTime(totalDuration)}
</span>
<div
ref={progressRef}
onMouseDown={handleMouseDown}
style={{
flex: 1,
height: 4,
background: "rgba(255,255,255,0.15)",
borderRadius: 2,
cursor: "pointer",
position: "relative",
}}
>
<div
style={{
height: "100%",
width: `${progressPercent}%`,
background: "#3b82f6",
borderRadius: 2,
transition: isDragging ? "none" : "width 0.1s linear",
}}
/>
<div
style={{
position: "absolute",
top: "50%",
left: `${progressPercent}%`,
transform: "translate(-50%, -50%)",
width: 10,
height: 10,
borderRadius: "50%",
background: "#3b82f6",
border: "2px solid #fff",
opacity: isDragging ? 1 : 0,
transition: "opacity 0.15s",
}}
/>
</div>
</div>
</>
)
}
export default FrontendPreviewPlayer
@@ -1,7 +1,9 @@
/**
* GeneratePage 步骤内容渲染
* 根据当前步骤渲染对应的 Step 组件
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 预览(4) → 标题(5) → 封面(6) → 确认(7)
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 标题(4) → 预览(5) → 封面(6) → 确认(7)
*
* V24: 移除 Step5 预览生成相关 props,改为纯标题样式编辑
*/
import React from "react"
import type { EditingTemplate } from "@/api/editing-planner"
@@ -9,12 +11,11 @@ import type { PresetVoiceItem } from "@/api/voices"
import type { VoiceClone } from "@/api/voice-clone"
import type { CoverConfig } from "../types/cover"
import type { TitleSettings } from "../types"
import type { PreviewItem, PreviewStatus } from "../hooks/useStep4Preview"
import Step1TemplateSelect from "../components/Step1TemplateSelect"
import Step2MaterialSelect from "../components/Step2MaterialSelect"
import Step3VoiceSelect from "../components/Step5VoiceSelect"
import Step4GeneratePreview from "../components/Step4GeneratePreview"
import Step5TitleSettings from "../components/Step4TitleSettings"
import Step5GeneratePreview from "../components/Step5GeneratePreview"
import Step4TitleSettings from "../components/Step4TitleSettings"
import Step6CoverSettings from "../components/Step6CoverSettings"
import Step7ConfirmGenerate from "../components/Step7ConfirmGenerate"
import type { GeneratedVideo } from "@/api/template-editor"
@@ -35,6 +36,17 @@ export interface GenerateStepContentProps {
/* 标题 */
titleSettings: TitleSettings
onTitleSettingsChange: (settings: TitleSettings) => void
/* 标题样式回调 — Step5 样式面板使用 */
onUpdatePosition: (position: string) => void
onUpdateFont: (font: string) => void
onUpdateSize: (size: number) => void
onToggleBold: () => void
onToggleItalic: () => void
onToggleStroke: () => void
onToggleShadow: () => void
onApplyPreset: (presetKey: string) => void
activePreset: string | null
titlePresets: { key: string; label: string; previewStyle: React.CSSProperties }[]
/* 封面 */
coverSettings: CoverConfig
onCoverSettingsChange: (settings: CoverConfig) => void
@@ -42,6 +54,7 @@ export interface GenerateStepContentProps {
/* 配音 */
selectedVoice: string
onSelectedVoiceChange: (id: string) => void
totalVideoDuration?: number
voiceMode: "preset" | "custom" | "clone"
onVoiceModeChange: (mode: "preset" | "custom" | "clone") => void
selectedClonedVoice: string
@@ -63,21 +76,6 @@ export interface GenerateStepContentProps {
onDismissError: () => void
/* 其他 */
presetVoices: PresetVoiceItem[]
videoRatio: 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
onGeneratePreview: () => void
onRegeneratePreview: () => void
}
export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) => {
@@ -94,11 +92,22 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onSmartSelectedIdsChange,
titleSettings,
onTitleSettingsChange,
onUpdatePosition,
onUpdateFont,
onUpdateSize,
onToggleBold,
onToggleItalic,
onToggleStroke,
onToggleShadow,
onApplyPreset,
activePreset,
titlePresets,
coverSettings,
onCoverSettingsChange,
duration,
selectedVoice,
onSelectedVoiceChange,
totalVideoDuration,
voiceMode,
selectedClonedVoice,
clonedVoices,
@@ -112,20 +121,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onRetry,
onDismissError,
presetVoices,
videoRatio,
previewCount,
onPreviewCountChange,
previewItems,
previewSelectedIndex,
onSelectPreview,
previewOverallStatus,
previewOverallError,
previewOverallProgress,
previewAnyGenerating,
previewTemplateName,
previewMaterialCount,
onGeneratePreview,
onRegeneratePreview,
} = props
switch (currentStep) {
@@ -153,33 +148,30 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
<Step3VoiceSelect
selectedVoice={selectedVoice}
onSelectedVoiceChange={onSelectedVoiceChange}
totalVideoDuration={totalVideoDuration}
/>
)
case 4:
return (
<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}
<Step4TitleSettings
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
/>
)
case 5:
return (
<Step5TitleSettings
<Step5GeneratePreview
titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange}
onUpdatePosition={onUpdatePosition}
onUpdateFont={onUpdateFont}
onUpdateSize={onUpdateSize}
onToggleBold={onToggleBold}
onToggleItalic={onToggleItalic}
onToggleStroke={onToggleStroke}
onToggleShadow={onToggleShadow}
onApplyPreset={onApplyPreset}
activePreset={activePreset}
titlePresets={titlePresets}
/>
)
case 6:
@@ -1,387 +1,250 @@
/**
* 右侧预览视频面板
* Step4 生成预览后常驻显示预览视频
* Step5+ 用 Canvas 绘制标题预览(替代 CSS overlay,与 ASS 渲染行为一致)
* Step4+: 显示预览视频面板
* Step5: 前端实时预览 — 用原生 video 播放素材片段 + CSS 标题叠加
*
* 设计说明:标题预览仅在有视频时显示(叠加在视频画面上方)。
* 无视频状态(idle/loading/error)下不再单独显示标题预览,这是有意为之的设计简化。
* 架构改造:完全去除后端 FFmpeg 预览依赖
* - 使用 FrontendPreviewPlayer 直接播放素材片段
* - TitleOverlay CSS 层实时响应标题样式变化
*
* Canvas 居中修复说明:
* Canvas 的 CSS 位置和尺寸直接匹配视频实际渲染区域(通过 getBoundingClientRect),
* 绘制坐标系基于 Canvas 自身尺寸,x = w/2 即可实现水平居中,
* 避免容器与视频尺寸不一致时浏览器拉伸 Canvas 导致居中偏移。
* 布局:本组件提供 .xx-preview-video 容器(position: relative + overflow: hidden
* FrontendPreviewPlayer 的内容通过 absolute 定位填充容器
* TitleOverlay 通过 absolute 定位 + z-index: 30 覆盖在最上层
*/
import React, { useRef, useEffect, useCallback } from "react"
import { PlayCircleOutlined, LoadingOutlined } from "@ant-design/icons"
import type { PreviewResult, PreviewStatus } from "../hooks/useStep4Preview"
import React, { useMemo, useRef, useState, useEffect } from "react"
import { LoadingOutlined } from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import type { TitleSettings } from "../types"
import FrontendPreviewPlayer from "./FrontendPreviewPlayer"
interface PreviewVideoPanelProps {
previewStatus: PreviewStatus
previewResult: PreviewResult | null
previewError: string
progress: number
/** 已加载的素材列表 */
assets: AssetItem[]
/** 当前模板 */
template: EditingTemplate | null
/** 视频比例 */
videoRatio: string
onRegenerate: () => void
/** 标题文字(Step5 起传入) */
titleText?: string
/** 标题样式设置(Step5 起传入) */
/** 素材是否已加载就绪 */
assetsReady: boolean
/** 素材是否正在加载 */
assetsLoading: boolean
/** 标题设置 — 用于 CSS 实时预览层 */
titleSettings?: TitleSettings
/** 配音音频 URL */
voiceAudioUrl?: string
}
/* ── Canvas 绘制工具函数 ── */
/* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */
const ASS_VIDEO_HEIGHT = 720
const ASS_TITLE_MARGIN_TOP = 60
const ASS_TITLE_MARGIN_BOTTOM = 60
const ASS_TITLE_MARGIN_SIDE = 40
/**
* 将文本按 maxWidth 逐字换行,返回行数组。
* 与 ASS 字幕引擎的逐字换行行为一致。
* 根据 position 计算 CSS 垂直定位
* 与后端 position_to_ass_alignment() 对齐:top→8, center→5, bottom→2
*/
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
}
function getPositionStyle(position: string): React.CSSProperties {
const sidePercent = (ASS_TITLE_MARGIN_SIDE / 1280) * 100
/**
* 在 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":
return {
bottom: `${(ASS_TITLE_MARGIN_BOTTOM / ASS_VIDEO_HEIGHT) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
case "center":
return {
top: "50%",
transform: "translateY(-50%)",
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
case "top":
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
return {
top: `${(ASS_TITLE_MARGIN_TOP / ASS_VIDEO_HEIGHT) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
}
}
/* ── 组件 ── */
/**
* 构建 CSS 标题层的样式
* 所有渲染参数与后端 FFmpeg ASS 字幕一致
*/
function buildTitleStyle(settings: TitleSettings, containerHeight: number): React.CSSProperties {
// 用 px 计算 fontSize,不再依赖父元素 font-size 的百分比
const fontSizePx =
containerHeight > 0
? (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * containerHeight
: (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * 400 // fallback
const base: React.CSSProperties = {
fontFamily: settings.font || "思源黑体",
fontSize: `${fontSizePx}px`,
color: settings.color || "#ffffff",
fontWeight: settings.bold ? 700 : 400,
fontStyle: settings.italic ? "italic" : "normal",
lineHeight: 1.3,
wordBreak: "break-word",
pointerEvents: "none",
userSelect: "none",
paddingLeft: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`,
paddingRight: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`,
}
if (settings.stroke) {
base.WebkitTextStroke = "1px #000000"
}
if (settings.shadow) {
base.textShadow = "2px 2px 4px rgba(0,0,0,0.8)"
}
return base
}
/**
* CSS 标题预览覆盖层
* 始终渲染:有标题显示标题,无标题显示占位文本"标题预览"
* z-index: 20(在视频 z-index:1 和控制条 z-index:10 之上)
*/
const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => {
const containerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(400) // fallback
// ResizeObserver 获取容器实际高度
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const h = entry.contentRect.height
if (h > 0) setContainerHeight(h)
}
})
ro.observe(el)
// 初始化也读一次
const rect = el.getBoundingClientRect()
if (rect.height > 0) setContainerHeight(rect.height)
return () => ro.disconnect()
}, [])
const positionStyle = useMemo(
() => getPositionStyle(titleSettings.position),
[titleSettings.position],
)
const titleStyle = useMemo(
() => buildTitleStyle(titleSettings, containerHeight),
// eslint-disable-next-line react-hooks/exhaustive-deps -- 已逐字段列出 titleSettings 依赖
[
containerHeight,
titleSettings.font,
titleSettings.size,
titleSettings.color,
titleSettings.bold,
titleSettings.italic,
titleSettings.stroke,
titleSettings.shadow,
],
)
const displayTitle = titleSettings.title?.trim() || "标题预览"
return (
<div
ref={containerRef}
style={{
position: "absolute",
inset: 0,
zIndex: 20,
pointerEvents: "none",
overflow: "hidden",
}}
>
<div
style={{
...positionStyle,
...titleStyle,
position: "absolute",
}}
>
{displayTitle}
</div>
</div>
)
}
/* ── 主组件 ── */
export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
previewStatus,
previewResult,
previewError,
progress,
assets,
template,
videoRatio,
onRegenerate,
titleText,
assetsReady,
assetsLoading,
titleSettings,
voiceAudioUrl,
}) => {
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])
const videoAspectStyle = { aspectRatio: (videoRatio || "9:16").replace(":", "/") }
return (
<div className="xx-generate-preview">
<div className="xx-preview-header">
<h3></h3>
{hasPreview && <span className="xx-preview-badge">480p </span>}
{assetsReady && assets.length > 0 && <span className="xx-preview-badge"></span>}
</div>
{/* 空状态:还没生成预览 */}
{previewStatus === "idle" && (
<div className="xx-preview-empty">
<PlayCircleOutlined
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
/>
<p className="xx-preview-empty-title"></p>
<p className="xx-preview-empty-desc"> 3 </p>
</div>
)}
{/* 生成中 */}
{isLoading && (
<div className="xx-preview-loading-panel">
<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 }}>
{previewStatus === "pending" ? "排队中..." : `生成中 ${progress}%`}
</p>
</div>
{/* ✅ 预览容器 — 唯一的 .xx-preview-video 容器
内部所有内容(视频、控制条、标题叠加层)通过 absolute 定位填充 */}
<div className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}>
{/* 加载中状态 */}
{assetsLoading && (
<div
className="xx-preview-loading-center"
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
zIndex: 5,
}}
>
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
<p style={{ marginTop: 12, color: "rgba(255,255,255,0.8)", fontSize: 14 }}>
...
</p>
</div>
<div className="xx-preview-progress-bar-wrap">
<div className="xx-preview-progress-fill" style={{ width: `${progress}%` }} />
</div>
</div>
)}
)}
{/* 生成失败 */}
{isError && (
<div className="xx-preview-error-panel">
<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">
{typeof previewError === "string" && previewError ? previewError : "请重试"}
</p>
<button className="xx-btn xx-btn-ghost xx-btn-block" onClick={onRegenerate}>
</button>
</div>
)}
{/* 前端播放器(视频 + 控制条 + 播放按钮)*/}
<FrontendPreviewPlayer
assets={assets}
template={template}
videoRatio={videoRatio}
ready={assetsReady}
voiceAudioUrl={voiceAudioUrl}
/>
{/* 预览成功 + Canvas 标题叠加 */}
{hasPreview && (
<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>
)}
{/* CSS 标题实时预览层 — z-index: 20,始终渲染在内容层之上 */}
{titleSettings && <TitleOverlay titleSettings={titleSettings} />}
</div>
{/* 预览信息 */}
{hasPreview && previewResult && (
{/* 素材信息 */}
{assetsReady && assets.length > 0 && (
<div className="xx-preview-info">
<div className="xx-preview-info-row">
<span></span>
<span>
{(typeof previewResult.duration === "number" ? previewResult.duration : 0).toFixed(1)}{" "}
</span>
</div>
<div className="xx-preview-info-row">
<span></span>
<span>{previewResult.clipCount} </span>
<span></span>
<span>{assets.length} </span>
</div>
<div className="xx-preview-info-row">
<span></span>
@@ -1,294 +0,0 @@
/**
* 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
@@ -1,13 +1,13 @@
/**
* Step 4
* + AI
* Step5
*/
import React from "react"
import { AutoComplete } from "antd"
import { POSITION_OPTIONS, FONT_OPTIONS } from "../constants"
import type { TitleSettings } from "../types"
import { useStep4Title } from "../hooks/useStep4Title"
import AiTitleGenerator from "./title/AiTitleGenerator"
import TitleStylePanel from "./title/TitleStylePanel"
interface Step4TitleSettingsProps {
titleSettings: TitleSettings
@@ -110,23 +110,6 @@ const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
</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}
/>
</div>
)
}
@@ -0,0 +1,84 @@
/**
* Step 5
*
* PreviewVideoPanel
*/
import React from "react"
import { PlayCircleOutlined } from "@ant-design/icons"
import { POSITION_OPTIONS, FONT_OPTIONS } from "../constants"
import type { TitleSettings } from "../types"
import TitleStylePanel from "./title/TitleStylePanel"
interface Step5GeneratePreviewProps {
/* 标题样式 */
titleSettings: TitleSettings
onUpdatePosition: (position: string) => void
onUpdateFont: (font: string) => void
onUpdateSize: (size: number) => void
onToggleBold: () => void
onToggleItalic: () => void
onToggleStroke: () => void
onToggleShadow: () => void
onApplyPreset: (presetKey: string) => void
activePreset: string | null
titlePresets: { key: string; label: string; previewStyle: React.CSSProperties }[]
}
const Step5GeneratePreview: React.FC<Step5GeneratePreviewProps> = ({
titleSettings,
onUpdatePosition,
onUpdateFont,
onUpdateSize,
onToggleBold,
onToggleItalic,
onToggleStroke,
onToggleShadow,
onApplyPreset,
activePreset,
titlePresets,
}) => {
return (
<div className="xx-form-section">
<h3>🎬 </h3>
{/* 前端实时预览提示 */}
<div
className="xx-preview-tip"
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: "12px 16px",
background: "rgba(59, 130, 246, 0.08)",
borderRadius: 8,
marginBottom: 16,
border: "1px solid rgba(59, 130, 246, 0.15)",
}}
>
<PlayCircleOutlined style={{ fontSize: 18, color: "#3b82f6" }} />
<span style={{ fontSize: 13, color: "var(--text-secondary, #666)" }}>
</span>
</div>
{/* 标题样式面板 */}
<TitleStylePanel
settings={titleSettings}
onUpdatePosition={onUpdatePosition}
onUpdateFont={onUpdateFont}
onUpdateSize={onUpdateSize}
onToggleBold={onToggleBold}
onToggleItalic={onToggleItalic}
onToggleStroke={onToggleStroke}
onToggleShadow={onToggleShadow}
onApplyPreset={onApplyPreset}
activePreset={activePreset}
titlePresets={titlePresets}
POSITION_OPTIONS={POSITION_OPTIONS}
FONT_OPTIONS={FONT_OPTIONS}
/>
</div>
)
}
export default Step5GeneratePreview
@@ -5,13 +5,15 @@
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 { AudioOutlined, SoundOutlined, WarningOutlined } from "@ant-design/icons"
import { Modal } from "antd"
import { getAssetsByKind } from "@/api/assets"
import type { AssetItem } from "@/api/assets"
interface Step5VoiceSelectProps {
selectedVoice: string
onSelectedVoiceChange: (id: string) => void
totalVideoDuration?: number
}
/** 格式化时长 mm:ss */
@@ -34,10 +36,13 @@ const formatFileSize = (bytes?: number): string => {
const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
selectedVoice,
onSelectedVoiceChange,
totalVideoDuration = 0,
}) => {
const navigate = useNavigate()
const [playingId, setPlayingId] = useState<string | null>(null)
const audioRef = useRef<HTMLAudioElement | null>(null)
const [durationWarningOpen, setDurationWarningOpen] = useState(false)
const [pendingVoiceId, setPendingVoiceId] = useState<string | null>(null)
// 获取用户上传的配音素材
const { data: materials = [], isLoading } = useQuery({
@@ -78,14 +83,38 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
[playingId],
)
/** 选中素材 */
/** 选中素材(含时长校验) */
const handleSelect = useCallback(
(id: string) => {
// 如果启用了时长校验,且配音时长不足
if (totalVideoDuration > 0) {
const material = materials.find((m) => m.id === id)
if (material && (material.duration || 0) < totalVideoDuration) {
setPendingVoiceId(id)
setDurationWarningOpen(true)
return
}
}
onSelectedVoiceChange(id)
},
[onSelectedVoiceChange],
[onSelectedVoiceChange, totalVideoDuration, materials],
)
/** 确认使用时长不足的配音 */
const handleConfirmUseAnyway = useCallback(() => {
if (pendingVoiceId) {
onSelectedVoiceChange(pendingVoiceId)
}
setDurationWarningOpen(false)
setPendingVoiceId(null)
}, [pendingVoiceId, onSelectedVoiceChange])
/** 取消选择 */
const handleCancelSelection = useCallback(() => {
setDurationWarningOpen(false)
setPendingVoiceId(null)
}, [])
/** 跳转到配音库上传 */
const handleGoToUpload = useCallback(() => {
navigate("/app/voices")
@@ -238,15 +267,65 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
justifyContent: "space-between",
fontSize: 12,
color: "#999",
alignItems: "center",
}}
>
<span>{formatDuration(item.duration)}</span>
<span style={{ display: "flex", alignItems: "center", gap: 4 }}>
{formatDuration(item.duration)}
{totalVideoDuration > 0 &&
(Number(item.duration) || 0) < Number(totalVideoDuration) && (
<span
style={{
color: "#ff4d4f",
fontSize: 11,
fontWeight: 500,
display: "inline-flex",
alignItems: "center",
gap: 2,
}}
>
<WarningOutlined />
</span>
)}
</span>
<span>{formatFileSize(item.file_size)}</span>
</div>
</div>
)
})}
</div>
{/* 时长不足警告弹窗 */}
<Modal
title={
<span style={{ display: "flex", alignItems: "center", gap: 8 }}>
<WarningOutlined style={{ color: "#faad14" }} />
</span>
}
open={durationWarningOpen}
onOk={handleConfirmUseAnyway}
onCancel={handleCancelSelection}
okText="仍要使用"
cancelText="重新选择"
okButtonProps={{ danger: true }}
>
{(() => {
const pendingMaterial = pendingVoiceId
? materials.find((m) => m.id === pendingVoiceId)
: null
return (
<p>
<strong>{pendingMaterial ? formatDuration(pendingMaterial.duration) : "--"}</strong>
<strong>{formatDuration(totalVideoDuration)}</strong>
</p>
)
})()}
</Modal>
</div>
)
}
@@ -30,7 +30,7 @@ export const UploadCoverPicker: React.FC<UploadCoverPickerProps> = ({ uploadUrl,
<div className="xx-cover-upload-placeholder">
<span style={{ fontSize: 32 }}>📤</span>
<span className="xx-cover-upload-text"></span>
<span className="xx-cover-upload-hint"> JPG / PNG 16:9 </span>
<span className="xx-cover-upload-hint"> JPG / PNG 9:16 </span>
</div>
)}
<input
+2 -2
View File
@@ -32,8 +32,8 @@ export const STEPS = [
{ key: 1, label: "选择模板" },
{ key: 2, label: "选择素材" },
{ key: 3, label: "选择配音" },
{ key: 4, label: "生成预览" },
{ key: 5, label: "选择标题" },
{ key: 4, label: "选择标题" },
{ key: 5, label: "生成预览" },
{ key: 6, label: "选择封面" },
{ key: 7, label: "确认生成" },
]
+11 -11
View File
@@ -891,17 +891,16 @@
/* ── 视频预览 ── */
.xx-preview-video {
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
max-height: 400px;
border-radius: var(--radius-md);
background: linear-gradient(135deg, var(--color-gray-900), var(--color-primary-900));
display: grid;
place-items: center;
color: var(--text-inverse);
font-size: 36px;
position: relative;
overflow: hidden;
margin-bottom: 16px;
isolation: isolate;
}
.xx-preview-video::before {
@@ -910,6 +909,7 @@
inset: 0;
background: radial-gradient(circle at 72% 28%, rgba(255, 255, 255, 0.2), transparent 40%);
pointer-events: none;
z-index: 0;
}
.xx-preview-video video {
@@ -1516,7 +1516,7 @@
.xx-smart-match-thumb {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
background: #f1f5f9;
overflow: hidden;
}
@@ -2144,7 +2144,7 @@
}
.xx-cover-frame-placeholder {
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: var(--radius-md);
display: flex;
@@ -2247,7 +2247,7 @@
}
.xx-cover-upload-area {
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
display: flex;
@@ -2308,7 +2308,7 @@
.xx-cover-preview-box {
position: relative;
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
overflow: hidden;
@@ -2666,7 +2666,7 @@
.xx-preview-video-wrapper .xx-preview-video {
max-width: 300px;
width: 100%;
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
margin-bottom: 0;
}
@@ -2772,7 +2772,7 @@
.xx-video-thumb {
position: relative;
aspect-ratio: 16 / 9;
aspect-ratio: 9 / 16;
background: var(--bg-tertiary);
overflow: hidden;
}
@@ -2881,11 +2881,11 @@
overflow: hidden;
}
.ant-modal-content {
.xx-preview-modal .ant-modal-content {
padding: 0 !important;
}
.ant-modal-close {
.xx-preview-modal .ant-modal-close {
color: #fff !important;
}
@@ -19,8 +19,6 @@ export interface UseGenerateVideoProps {
autoSubtitles: boolean
bgm: boolean
generateCount: number
/** 预览任务的 task_id(用于新确认生成 API */
previewTaskId: string
}
/** 生成阶段 */
@@ -0,0 +1,935 @@
/**
* Canvas + WebCodecs Hook
* MP4 mp4box.js VideoDecoder Canvas
*
* WebCodecs hasSupport=false fallback
*/
import { useRef, useCallback, useEffect, useState } from "react"
import { createFile } from "mp4box"
import type { Movie, Sample } from "mp4box"
// ── 常量 ──
/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */
const MAX_INIT_FRAMES = 60
// ── MP4 Box 解析辅助函数 ──
// MP4 标准容器 box 列表(递归时会进入这些 box 内部搜索子 box)
const MP4_CONTAINER_TYPES = [
"moov",
"trak",
"mdia",
"minf",
"stbl",
"stsd",
"dinf",
"edts",
"udta",
"meta",
"tref",
]
const VISUAL_SAMPLE_ENTRY_TYPES = ["avc1", "avc3", "hvc1", "hev1"]
/**
* box hvcC avcC box 8 box header
*
* MP4 box moov trak mdia minf stbl stsd hev1 hvcC
* - box offset+8
* - stsd 8 version/flags 4B + entry_count 4B offset+16
* - VisualSampleEntry (avc1/avc3/hvc1/hev1) 78 box offset+8+78
*/
function findCodecConfigRecursive(
buffer: ArrayBuffer,
start: number,
end: number,
): ArrayBuffer | undefined {
const view = new DataView(buffer)
let offset = start
while (offset < end - 8) {
const size = view.getUint32(offset)
if (size < 8 || offset + size > end) break
const type = String.fromCharCode(
view.getUint8(offset + 4),
view.getUint8(offset + 5),
view.getUint8(offset + 6),
view.getUint8(offset + 7),
)
// 找到目标 codec 配置 box,返回内容(不含 8 字节 header)
if (type === "avcC" || type === "hvcC") {
console.log("[findCodecConfig] Found", type, "at offset", offset, "size", size)
return buffer.slice(offset + 8, offset + size)
}
// VisualSampleEntry:前 78 字节是固定字段,子 box 在 78 字节之后
if (VISUAL_SAMPLE_ENTRY_TYPES.includes(type)) {
const childResult = findCodecConfigRecursive(buffer, offset + 8 + 78, offset + size)
if (childResult) return childResult
}
// stsd:额外 8 字节头(version/flags 4B + entry_count 4B),子 box 在 offset+16
else if (type === "stsd") {
const childResult = findCodecConfigRecursive(buffer, offset + 8 + 8, offset + size)
if (childResult) return childResult
}
// 标准容器 box:从 offset+8 开始递归
else if (MP4_CONTAINER_TYPES.includes(type)) {
const childResult = findCodecConfigRecursive(buffer, offset + 8, offset + size)
if (childResult) return childResult
}
offset += size
}
return undefined
}
// ── 帧队列(环形缓冲区) ──
interface FrameEntry {
frame: VideoFrame
pts: number // 全局时间戳(秒),已按片段偏移对齐
duration: number // 帧持续时长(秒)
}
class FrameQueue {
private frames: FrameEntry[] = []
private maxSize: number
constructor(maxSize = 5) {
this.maxSize = maxSize
}
push(entry: FrameEntry) {
while (this.frames.length >= this.maxSize) {
const old = this.frames.shift()
old?.frame.close()
}
this.frames.push(entry)
}
/** 获取当前时间戳应显示的帧 */
getCurrentFrame(timestamp: number): VideoFrame | null {
let best: FrameEntry | null = null
let bestIdx = -1
for (let i = 0; i < this.frames.length; i++) {
const f = this.frames[i]
if (f.pts <= timestamp + 0.01) {
best = f
bestIdx = i
}
}
for (let i = 0; i < bestIdx; i++) {
this.frames[i].frame.close()
}
if (bestIdx >= 0) {
this.frames = this.frames.slice(bestIdx)
}
return best?.frame ?? null
}
clear() {
for (const f of this.frames) {
f.frame.close()
}
this.frames = []
}
get size() {
return this.frames.length
}
}
// ── 片段元数据 ──
interface SegmentMeta {
assetId: string
videoUrl: string
/** 该片段在全局时间轴上的起始时间(秒) */
globalStartTime: number
/** 该片段在全局时间轴上的结束时间(秒) */
globalEndTime: number
/** 视频轨道 ID */
trackId: number
/** 视频轨道 timescale */
timescale: number
/** 编解码器 */
codec: string
/** 视频宽度(像素) */
videoWidth: number
/** 视频高度(像素) */
videoHeight: number
/** 解码器配置数据(HEVC hvcC / H.264 avcC),WebCodecs 必需 */
description?: ArrayBuffer
/** 前端提取的样本数据(已按时间范围过滤,从关键帧开始) */
samples: Sample[]
}
// ── 播放器状态 ──
export interface CanvasPlayerState {
hasSupport: boolean
isPlaying: boolean
currentTime: number
duration: number
isReady: boolean
isBuffering: boolean
}
export interface CanvasPlayerControls {
play: () => void
pause: () => void
seek: (time: number) => void
destroy: () => void
}
interface SegmentSource {
assetId: string
videoUrl: string
startTime: number
endTime: number
}
/** 检测浏览器是否支持 WebCodecs VideoDecoder */
export function isWebCodecsSupported(): boolean {
return typeof window !== "undefined" && "VideoDecoder" in window && "VideoFrame" in window
}
/**
* useCanvasPlayer Canvas + WebCodecs
*/
export function useCanvasPlayer(
canvasRef: React.RefObject<HTMLCanvasElement | null>,
segments: SegmentSource[],
titleSettings?: {
text: string
fontSize: number
fontFamily: string
color: string
position: "top" | "center" | "bottom"
bold?: boolean
stroke?: boolean
shadow?: boolean
},
) {
const [state, setState] = useState<CanvasPlayerState>({
hasSupport: isWebCodecsSupported(),
isPlaying: false,
currentTime: 0,
duration: 0,
isReady: false,
isBuffering: false,
})
// ── 内部引用 ──
const decoderRef = useRef<VideoDecoder | null>(null)
const frameQueueRef = useRef(new FrameQueue(600))
/** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */
const decodedSegmentsRef = useRef(new Set<number>())
/** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */
const decodeGenerationRef = useRef(0)
const rafRef = useRef<number>(0)
const playStartRef = useRef<number>(0)
const playStartOffsetRef = useRef<number>(0)
const segmentDataRef = useRef<Map<string, ArrayBuffer>>(new Map())
const segmentMetaRef = useRef<SegmentMeta[]>([])
const videoDimRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 })
const isDestroyedRef = useRef(false)
const lastProgressUpdateRef = useRef<number>(0)
// 计算总时长
const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0)
// ── 加载 MP4 文件数据 ──
const loadSegment = useCallback(async (segment: SegmentSource): Promise<void> => {
if (isDestroyedRef.current) return
if (segmentDataRef.current.has(segment.assetId)) return
setState((s) => ({ ...s, isBuffering: true }))
try {
const resp = await fetch(segment.videoUrl)
const buffer = await resp.arrayBuffer()
segmentDataRef.current.set(segment.assetId, buffer)
} catch (err) {
console.error("[useCanvasPlayer] Failed to fetch segment:", err)
} finally {
setState((s) => ({ ...s, isBuffering: false }))
}
}, [])
// ── 从 MP4 buffer 提取编解码器配置数据(avcC / hvcC) ──
// WebCodecs VideoDecoder 对 HEVC/H.265 必须提供 description 字段
const extractCodecDescription = useCallback((buffer: ArrayBuffer): ArrayBuffer | undefined => {
try {
const view = new DataView(buffer)
let offset = 0
// 查找 moov box
while (offset < buffer.byteLength - 8) {
const size = view.getUint32(offset)
const type = String.fromCharCode(
view.getUint8(offset + 4),
view.getUint8(offset + 5),
view.getUint8(offset + 6),
view.getUint8(offset + 7),
)
if (type === "moov") {
const result = findCodecConfigRecursive(buffer, offset + 8, offset + size)
console.log("[useCanvasPlayer] extractCodecDescription:", {
moovOffset: offset,
moovSize: size,
searchRange: [offset + 8, offset + size],
found: !!result,
resultByteLength: result?.byteLength,
})
return result
}
if (size === 0) break
offset += size
}
} catch (e) {
console.warn("[useCanvasPlayer] extractCodecDescription failed:", e)
}
return undefined
}, [])
// ── 解封装单个片段,提取轨道元数据 + 按时间范围过滤样本 ──
// ✅ 关键修复:改为异步函数,等待 MP4Box.js 的 onSamples 回调完成后再返回
const demuxSegment = useCallback(
async (buffer: ArrayBuffer, segIndex: number): Promise<SegmentMeta | null> => {
const segment = segments?.[segIndex]
if (!segment) {
console.warn("[useCanvasPlayer] No segment at index", segIndex)
return null
}
// 计算全局偏移
let globalStart = 0
for (let i = 0; i < segIndex; i++) {
globalStart += segments[i].endTime - segments[i].startTime
}
const mp4File = createFile()
return new Promise<SegmentMeta | null>((resolve) => {
let meta: SegmentMeta | null = null
let resolved = false
// ✅ 超时保护:5秒后如果 onSamples 没有触发,返回 null
const timeout = setTimeout(() => {
if (!resolved) {
console.error(
`[useCanvasPlayer] Timeout: onSamples not triggered for segment ${segIndex}`,
)
resolved = true
resolve(null)
}
}, 5000)
mp4File.onReady = (info: Movie) => {
const videoTrack = info?.videoTracks?.[0]
console.log("[useCanvasPlayer] demuxSegment:", {
segIndex,
startTime: segment.startTime,
endTime: segment.endTime,
nbSamples: videoTrack?.nb_samples,
codec: videoTrack?.codec,
videoWidth: videoTrack?.track_width,
videoHeight: videoTrack?.track_height,
})
if (!videoTrack) {
console.warn("[useCanvasPlayer] No video track found for segment", segIndex)
clearTimeout(timeout)
resolved = true
resolve(null)
return
}
// 提取编解码器配置数据(HEVC 必需,H.264 也需要)
const description = extractCodecDescription(buffer)
// ✅ 如果 description 缺失,无法解码 HEVC
if (!description) {
console.error(
`[useCanvasPlayer] No description found for segment ${segIndex}, cannot decode HEVC`,
)
clearTimeout(timeout)
resolved = true
resolve(null)
return
}
meta = {
assetId: segment.assetId,
videoUrl: segment.videoUrl,
globalStartTime: globalStart,
globalEndTime: globalStart + (segment.endTime - segment.startTime),
trackId: videoTrack.id ?? 1,
timescale: videoTrack.timescale ?? 90000,
codec: videoTrack.codec ?? "avc1.42E01E",
videoWidth: videoTrack.track_width || 1280,
videoHeight: videoTrack.track_height || 720,
description,
samples: [],
}
// 提取所有 samples
mp4File.setExtractionOptions(videoTrack.id ?? 1, null, {
nbSamples: Infinity, // 提取所有 sample
})
mp4File.start()
}
mp4File.onSamples = (_trackId: number, _user: unknown, samples: Sample[]) => {
if (resolved) return // ✅ 防止重复 resolve
if (!meta) {
clearTimeout(timeout)
resolved = true
resolve(null)
return
}
// 前端切片:按 [startTime, endTime] 时间范围过滤样本
const timescale = meta.timescale
const startCts = segment.startTime * timescale
const endCts = segment.endTime * timescale
// 过滤出时间范围内的样本
let filtered = samples.filter((s) => (s?.cts ?? 0) >= startCts && (s?.cts ?? 0) < endCts)
// 确保从关键帧开始(跳过第一个 sync 之前的非关键帧)
let foundSync = false
filtered = filtered.filter((s) => {
if (s.is_sync) {
foundSync = true
return true
}
return foundSync
})
// Fallback:如果时间范围内没有样本,使用全部样本从第一个关键帧开始
if (filtered.length === 0) {
console.warn(
`[useCanvasPlayer] No samples in range [${segment.startTime}s, ${segment.endTime}s] for segment ${segIndex}, fallback to all from keyframe`,
)
let sync = false
filtered = samples.filter((s) => {
if (s.is_sync) {
sync = true
return true
}
return sync
})
}
meta.samples = filtered
console.log(
`[useCanvasPlayer] Segment ${segIndex}: ${filtered.length}/${samples.length} samples (range ${segment.startTime}s-${segment.endTime}s)`,
)
// ✅ 关键修复:等待 onSamples 完成后再返回
clearTimeout(timeout)
resolved = true
resolve(meta)
}
mp4File.onError = (_module: string, message: string) => {
console.error(`[useCanvasPlayer] MP4Box error: ${message}`)
clearTimeout(timeout)
resolved = true
resolve(null)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(buffer as any).fileStart = 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mp4File.appendBuffer(buffer as any)
})
},
[segments, extractCodecDescription],
)
// ── 初始化 VideoDecoder 并解码指定片段 ──
const decodeSegment = useCallback(
async (_buffer: ArrayBuffer, meta: SegmentMeta, maxFrames?: number): Promise<void> => {
if (isDestroyedRef.current) return
let decoderReady = false
// 配置解码器(每个片段可能需要不同的 codec/分辨率)
const decoder = new VideoDecoder({
output: (frame: VideoFrame) => {
// 从第一帧获取实际尺寸
if (videoDimRef.current.width === 0 || videoDimRef.current.height === 0) {
videoDimRef.current = { width: frame.codedWidth, height: frame.codedHeight }
console.log(
`[useCanvasPlayer] Actual frame size: ${frame.codedWidth}x${frame.codedHeight}`,
)
}
const localTime = frame.timestamp / 1_000_000
const globalTime = localTime + meta.globalStartTime
frameQueueRef.current.push({
frame,
pts: globalTime,
duration: (frame.duration ?? 0) / 1_000_000,
})
},
error: (e: DOMException) => {
console.error("[useCanvasPlayer] Decoder error:", e)
},
})
console.log("[useCanvasPlayer] configure:", {
codec: meta.codec,
description: meta.description,
descriptionByteLength: meta.description?.byteLength,
videoWidth: meta.videoWidth,
videoHeight: meta.videoHeight,
})
try {
await decoder.configure({
codec: meta.codec,
...(meta.description ? { description: meta.description } : {}),
})
decoderRef.current = decoder
decoderReady = true
// 标记缓冲结束,让 UI 开始渲染
setState((s) => ({ ...s, isBuffering: false }))
} catch (err) {
console.error("[useCanvasPlayer] Decoder configure failed for segment:", err)
return
}
if (!decoderReady) return
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片)
const samplesCollected = meta.samples
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: ${samplesCollected.length} samples to decode`,
)
if (samplesCollected.length === 0) {
console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId)
return
}
// 送入解码器
let decodedCount = 0
let skippedCount = 0
let decodeErrors = 0
for (const sample of samplesCollected) {
if (!sample.data || isDestroyedRef.current) {
skippedCount++
continue
}
if (decoder.state === "closed") break
// 初始化阶段限制解码帧数,避免帧缓冲溢出
if (maxFrames && decodedCount >= maxFrames) {
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: init decode limited to ${maxFrames} frames`,
)
break
}
const chunk = new EncodedVideoChunk({
type: sample.is_sync ? "key" : "delta",
timestamp: ((sample.cts ?? 0) / (meta.timescale || 90000)) * 1_000_000,
duration: ((sample.duration ?? 0) / (meta.timescale || 90000)) * 1_000_000,
data: sample.data,
})
try {
await decoder.decode(chunk) // 修复:await 捕获异步错误
decodedCount++
} catch (e) {
decodeErrors++
console.warn(`[useCanvasPlayer] Decode chunk error (${decodeErrors}):`, e)
// 连续 3 次解码失败,放弃当前片段
if (decodeErrors >= 3) {
console.error("[useCanvasPlayer] Too many decode errors, aborting segment")
break
}
}
}
console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: decoded ${decodedCount}, skipped ${skippedCount}, errors ${decodeErrors}, decoder.state=${decoder.state}`,
)
// flush 仅在解码器状态正常时执行
if (decoder.state === "configured") {
try {
await decoder.flush()
console.log(`[useCanvasPlayer] Segment ${meta.assetId}: flush complete`)
} catch (e) {
console.warn("[useCanvasPlayer] Decoder flush error:", e)
}
}
},
[],
)
/**
* ±1
*
* 使"先标记再解码"
*/
const decodeAroundPosition = useCallback(
async (currentTime: number) => {
const metas = segmentMetaRef.current
if (!metas || metas.length === 0) return
// 记录当前代数,seek 后代数变化则中止
const gen = decodeGenerationRef.current
let targetIdx = -1
let acc = 0
for (let i = 0; i < metas.length; i++) {
const dur = metas[i].globalEndTime - metas[i].globalStartTime
if (currentTime < acc + dur) {
targetIdx = i
break
}
acc += dur
}
if (targetIdx === -1) targetIdx = metas.length - 1
for (
let i = Math.max(0, targetIdx - 1);
i <= Math.min(metas.length - 1, targetIdx + 1);
i++
) {
// seek 已作废当前解码任务
if (decodeGenerationRef.current !== gen) return
if (decodedSegmentsRef.current.has(i)) continue
const meta = metas[i]
const buffer = segmentDataRef.current.get(meta.assetId)
if (!buffer) continue
// 先标记为解码中,防止下一帧渲染时重复发起解码
decodedSegmentsRef.current.add(i)
try {
await decodeSegment(buffer, meta, 300)
} catch (e) {
// 解码失败则移除标记,允许后续重试
decodedSegmentsRef.current.delete(i)
console.warn(`[useCanvasPlayer] 按需解码片段 ${i} 失败:`, e)
}
// await 后再次检查代数,seek 期间不更新标记
if (decodeGenerationRef.current !== gen) return
}
},
[decodeSegment],
)
// ── 标题绘制 ──
const drawTitle = useCallback(
(
ctx: CanvasRenderingContext2D,
canvas: HTMLCanvasElement,
title: NonNullable<typeof titleSettings>,
) => {
const fontSize = (title.fontSize / 720) * canvas.height
ctx.font = `${title.bold ? "bold" : "normal"} ${fontSize}px ${title.fontFamily}`
ctx.fillStyle = title.color
ctx.textAlign = "center"
// 按 "/" 分割为多行("/" 作为手动换行符)
const lines = title.text.split(/[//⁄∕]/)
console.log("[drawTitle] 原始标题:", JSON.stringify(title.text), "分割后:", lines)
const lineHeight = fontSize * 1.3
const totalHeight = lines.length * lineHeight
// 根据 position 计算第一行的 Y 坐标
let startY: number
switch (title.position) {
case "top":
startY = fontSize + canvas.height * 0.08
break
case "bottom":
startY = canvas.height - canvas.height * 0.08 - totalHeight + lineHeight
break
case "center":
default:
startY = (canvas.height - totalHeight) / 2 + lineHeight
break
}
if (title.shadow) {
ctx.shadowColor = "rgba(0,0,0,0.8)"
ctx.shadowBlur = 4
ctx.shadowOffsetX = 2
ctx.shadowOffsetY = 2
}
lines.forEach((line, idx) => {
const y = startY + idx * lineHeight
if (title.stroke) {
ctx.strokeStyle = "#000000"
ctx.lineWidth = 1
ctx.strokeText(line, canvas.width / 2, y)
}
ctx.fillText(line, canvas.width / 2, y)
})
ctx.shadowColor = "transparent"
ctx.shadowBlur = 0
ctx.shadowOffsetX = 0
ctx.shadowOffsetY = 0
},
[],
)
// ── 计算保持宽高比的绘制矩形(letterbox / pillarbox ──
const computeDrawRect = useCallback(
(canvasW: number, canvasH: number): { dx: number; dy: number; dw: number; dh: number } => {
const vw = videoDimRef.current.width
const vh = videoDimRef.current.height
if (vw <= 0 || vh <= 0) return { dx: 0, dy: 0, dw: canvasW, dh: canvasH }
const canvasAspect = canvasW / canvasH
const videoAspect = vw / vh
let dw: number, dh: number
if (canvasAspect > videoAspect) {
// canvas 更宽 → pillarbox(左右留黑)
dh = canvasH
dw = canvasH * videoAspect
} else {
// canvas 更高 → letterbox(上下留黑)
dw = canvasW
dh = canvasW / videoAspect
}
return {
dx: (canvasW - dw) / 2,
dy: (canvasH - dh) / 2,
dw,
dh,
}
},
[],
)
// ── Canvas 渲染循环 ──
const renderFrame = useCallback(() => {
if (isDestroyedRef.current) return
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return
const elapsed = (performance.now() - playStartRef.current) / 1000
const currentTime = Math.min(playStartOffsetRef.current + elapsed, totalDuration)
// getCurrentFrame 返回 FrameQueue 内部引用,帧生命周期由 FrameQueue 管理
// push 淘汰旧帧时 close、clear 时全部 close),渲染层不应 close
const frame = frameQueueRef.current.getCurrentFrame(currentTime)
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (frame) {
const rect = computeDrawRect(canvas.width, canvas.height)
ctx.drawImage(frame, rect.dx, rect.dy, rect.dw, rect.dh)
}
if (titleSettings?.text) {
drawTitle(ctx, canvas, titleSettings)
}
// 进度更新节流到 200ms5fps),减少 React re-render
const now = performance.now()
if (now - lastProgressUpdateRef.current >= 200) {
lastProgressUpdateRef.current = now
setState((s) => {
if (Math.abs(s.currentTime - currentTime) > 0.01) {
return { ...s, currentTime }
}
return s
})
// 按需解码当前 ±1 片段
decodeAroundPosition(currentTime)
}
if (currentTime >= totalDuration) {
setState((s) => ({ ...s, isPlaying: false }))
return
}
rafRef.current = requestAnimationFrame(renderFrame)
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, decodeAroundPosition])
// ── 播放控制 ──
const play = useCallback(async () => {
if (!state.hasSupport || isDestroyedRef.current) return
// 重播场景:currentTime 已回到起点但 decodedSegmentsRef 仍有旧标记
// 此时 FrameQueue 中旧帧已被淘汰,需清空标记让 decodeAroundPosition 重新解码
if (state.currentTime <= 0.1 && decodedSegmentsRef.current.size > 0) {
decodeGenerationRef.current++
decodedSegmentsRef.current.clear()
// 同步清空帧缓冲,避免旧帧残留导致 getCurrentFrame 返回 null
frameQueueRef.current.clear()
}
setState((s) => ({ ...s, isPlaying: true }))
playStartRef.current = performance.now()
playStartOffsetRef.current = state.currentTime
lastProgressUpdateRef.current = 0
rafRef.current = requestAnimationFrame(renderFrame)
// 立即触发一次按需解码,不等渲染循环 200ms 节流
decodeAroundPosition(state.currentTime)
}, [state.hasSupport, state.currentTime, renderFrame, decodeAroundPosition])
const pause = useCallback(() => {
setState((s) => ({ ...s, isPlaying: false }))
cancelAnimationFrame(rafRef.current)
}, [])
const seek = useCallback(
async (time: number) => {
const clampedTime = Math.max(0, Math.min(time, totalDuration))
setState((s) => ({ ...s, currentTime: clampedTime }))
playStartOffsetRef.current = clampedTime
playStartRef.current = performance.now()
// seek 时递增解码代数,作废正在进行的异步解码
decodeGenerationRef.current++
// 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记
frameQueueRef.current.clear()
decodedSegmentsRef.current.clear()
await decodeAroundPosition(clampedTime)
},
[totalDuration, decodeAroundPosition],
)
const destroy = useCallback(() => {
isDestroyedRef.current = true
cancelAnimationFrame(rafRef.current)
if (decoderRef.current && decoderRef.current.state !== "closed") {
decoderRef.current.close()
}
// 递增代数中止进行中的异步解码,清空帧队列(clear 内部 close 所有帧)
decodeGenerationRef.current++
frameQueueRef.current.clear()
segmentDataRef.current.clear()
segmentMetaRef.current = []
decodedSegmentsRef.current.clear()
}, [])
// ── 预加载下一个片段的数据 ──
const preloadNext = useCallback(
async (currentIndex: number) => {
const nextIdx = currentIndex + 1
if (nextIdx >= segments.length) return
const next = segments[nextIdx]
if (segmentDataRef.current.has(next.assetId)) return
await loadSegment(next)
},
[segments, loadSegment],
)
// ── 初始化:加载并解码所有片段 ──
useEffect(() => {
if (!state.hasSupport || segments.length === 0) {
console.log("[useCanvasPlayer] Skip init:", {
hasSupport: state.hasSupport,
segmentCount: segments.length,
})
return
}
let cancelled = false
console.log("[useCanvasPlayer] Init start, segments:", segments.length)
const init = async () => {
setState((s) => ({ ...s, isBuffering: true }))
// 1. 加载所有片段数据
for (const seg of segments) {
await loadSegment(seg)
if (cancelled) {
console.log("[useCanvasPlayer] Cancelled during loadSegment")
return
}
}
// 验证 buffer 是否都已存入
const bufferCheck = segments.map((s) => ({
assetId: s.assetId,
hasBuffer: segmentDataRef.current.has(s.assetId),
}))
console.log("[useCanvasPlayer] Buffers loaded:", bufferCheck)
// 2. 解析每个片段的轨道元数据(await 等待 onSamples 回调完成)
const metas: SegmentMeta[] = []
for (let i = 0; i < segments.length; i++) {
const buffer = segmentDataRef.current.get(segments[i].assetId)
if (!buffer) {
console.warn("[useCanvasPlayer] No buffer for segment", i, segments[i].assetId)
continue
}
const meta = await demuxSegment(buffer, i)
if (cancelled) {
console.log("[useCanvasPlayer] Cancelled during demuxSegment")
return
}
if (meta) metas.push(meta)
}
if (cancelled || metas.length === 0) {
console.warn("[useCanvasPlayer] Init failed:", { cancelled, metasCount: metas.length })
setState((s) => ({ ...s, isBuffering: false }))
return
}
segmentMetaRef.current = metas
// 3. 按需解码:初始只解码前 3 个片段,后续通过 decodeAroundPosition 动态加载
// 避免一次性全量解码导致 frameQueue 环形缓冲区旧帧被丢弃引发黑屏
decodedSegmentsRef.current.clear()
const initGen = decodeGenerationRef.current
const initialDecodeCount = Math.min(metas.length, 3)
for (let i = 0; i < initialDecodeCount; i++) {
if (cancelled) break
// seek 或 destroy 已作废当前初始化
if (decodeGenerationRef.current !== initGen) break
const meta = metas[i]
const buffer = segmentDataRef.current.get(meta.assetId)
if (!buffer) continue
// 先标记为解码中,防止重复解码
decodedSegmentsRef.current.add(i)
try {
await decodeSegment(buffer, meta, MAX_INIT_FRAMES)
} catch (e) {
// 解码失败则移除标记,允许后续重试
decodedSegmentsRef.current.delete(i)
console.warn(`[useCanvasPlayer] 初始化解码片段 ${i} 失败:`, e)
}
}
if (!cancelled) {
console.log("[useCanvasPlayer] Init complete, isReady = true")
setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false }))
}
}
init()
return () => {
cancelled = true
destroy()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segments, state.hasSupport])
return {
state: { ...state, duration: totalDuration },
controls: { play, pause, seek, destroy } satisfies CanvasPlayerControls,
preloadNext,
}
}
export default useCanvasPlayer
@@ -5,7 +5,7 @@
import { useState, useCallback } from "react"
import { message } from "antd"
import type { GeneratedVideo } from "@/api/template-editor"
import { confirmGeneration } from "@/api/generation"
import { createGenerationTask } from "@/api/tasks/tasks"
import type { UseGenerateVideoProps } from "./generate-video/types"
import { getGenerationPhase } from "./generate-video/phase"
import { useGenerationPolling } from "./generate-video/useGenerationPolling"
@@ -55,17 +55,65 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
clearTimer()
try {
// 使用确认生成 API(基于预览任务)
// 解析分辨率
const [widthStr, heightStr] = (props.videoRatio || "1080x1920").split("x")
const outputWidth = parseInt(widthStr, 10) || 1080
const outputHeight = parseInt(heightStr, 10) || 1920
const ratio = props.videoRatio || "9:16"
let outputWidth: number
let outputHeight: number
await confirmGeneration(props.previewTaskId, {
if (ratio.includes(":")) {
const [rw, rh] = ratio.split(":").map(Number)
if (rw > 0 && rh > 0) {
const [longSide, shortSide] = rw < rh ? [rh, rw] : [rw, rh]
const baseLong = 1920
const baseShort = Math.round((baseLong * shortSide) / longSide)
const evenShort = baseShort - (baseShort % 2)
if (rw < rh) {
outputWidth = evenShort
outputHeight = baseLong
} else {
outputWidth = baseLong
outputHeight = evenShort
}
} else {
outputWidth = 1080
outputHeight = 1920
}
} else if (ratio.includes("x")) {
const [wStr, hStr] = ratio.split("x")
outputWidth = parseInt(wStr, 10) || 1080
outputHeight = parseInt(hStr, 10) || 1920
} else {
outputWidth = 1080
outputHeight = 1920
}
const assetIds =
props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials
// 直接创建正式生成任务
await createGenerationTask({
template_id: selectedTemplate,
asset_ids: assetIds,
output_width: outputWidth,
output_height: outputHeight,
cover_url: props.coverSettings.upload_url || "",
custom_title: props.titleSettings.title || "",
cover_url: props.coverSettings?.upload_url || "",
custom_title: props.titleSettings?.title || "",
duration: props.duration || undefined,
video_ratio: props.videoRatio,
...(props.titleSettings?.title
? {
title_config: {
text: props.titleSettings.title,
font: props.titleSettings.font,
font_size: props.titleSettings.size,
font_color: props.titleSettings.color,
position: props.titleSettings.position,
bold: props.titleSettings.bold,
stroke: props.titleSettings.stroke,
shadow: props.titleSettings.shadow,
},
}
: {}),
})
startPolling()
@@ -78,7 +126,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
setGenerateError(finalMsg)
message.error(finalMsg)
}
}, [props, clearTimer, startPolling])
}, [props, clearTimer, startPolling, selectedTemplate])
/* 重新生成(失败后重试) */
const retry = useCallback(() => {
@@ -0,0 +1,114 @@
/**
* Hook
* ID file_urlduration
* 使
*
* /assets/batch 405
* 使 Promise.allSettled GET /assets/{id}
*/
import { useState, useEffect, useCallback, useRef } from "react"
import type { AssetItem } from "@/api/assets"
import type { AxiosResponse } from "axios"
/**
* ID
* 使 Promise.allSettled
*/
async function fetchAssetsByIds(ids: string[]): Promise<AssetItem[]> {
if (!ids.length) return []
try {
const { default: apiClient } = await import("@/api/client")
const results = await Promise.allSettled(
ids.map((id) => apiClient.get<AssetItem>(`/assets/${id}`)),
)
return results
.filter(
(r): r is PromiseFulfilledResult<AxiosResponse<AssetItem>> =>
r.status === "fulfilled" && !!r.value?.data,
)
.map((r) => r.value.data)
} catch {
return []
}
}
interface UsePreviewAssetsReturn {
/** 加载后的素材列表 */
assets: AssetItem[]
/** 是否正在加载 */
loading: boolean
/** 是否已就绪(加载完成) */
ready: boolean
/** 手动触发重新加载 */
reload: () => void
}
/**
* usePreviewAssets
*/
export function usePreviewAssets(assetIds: string[], enabled: boolean): UsePreviewAssetsReturn {
const [assets, setAssets] = useState<AssetItem[]>([])
const [loading, setLoading] = useState(false)
const [ready, setReady] = useState(false)
const requestIdRef = useRef(0)
// 稳定化 assetIds:只有内容真正变化时才更新引用
const stableAssetIds = useStableArray(assetIds)
const load = useCallback(async () => {
if (!stableAssetIds.length || !enabled) {
setAssets([])
setReady(false)
return
}
const thisRequestId = ++requestIdRef.current
setLoading(true)
setReady(false)
try {
const result = await fetchAssetsByIds(stableAssetIds)
// 防止竞态:只保留最新请求的结果
if (requestIdRef.current === thisRequestId) {
setAssets(result)
setReady(result.length > 0)
}
} catch {
if (requestIdRef.current === thisRequestId) {
setAssets([])
setReady(false)
}
} finally {
if (requestIdRef.current === thisRequestId) {
setLoading(false)
}
}
}, [stableAssetIds, enabled])
useEffect(() => {
load()
}, [load])
return { assets, loading, ready, reload: load }
}
/**
* useStableArray Hook
* re-render
*/
function useStableArray<T>(array: T[]): T[] {
const ref = useRef<T[]>(array)
// 比较数组内容是否真正变化
const hasChanged =
array.length !== ref.current.length || array.some((item, index) => item !== ref.current[index])
if (hasChanged) {
ref.current = array
}
return ref.current
}
export default usePreviewAssets
@@ -0,0 +1,362 @@
/**
* Hook video v2
* <video> display
* video + src
*/
import { useState, useRef, useCallback, useEffect, useMemo } from "react"
/** 单个播放片段 */
export interface PlaybackSegment {
/** 素材 ID */
assetId: string
/** 素材视频 URL */
videoUrl: string
/** 片段在素材中的入点(秒) */
startTime: number
/** 片段在素材中的出点(秒) */
endTime: number
/** 片段在时间线中的顺序 */
order: number
}
/** 调度器返回 */
export interface SegmentSchedulerState {
/** 是否正在播放 */
isPlaying: boolean
/** 当前播放的全局时间(秒) */
currentTime: number
/** 总时长(秒) */
totalDuration: number
/** 当前片段索引 */
currentSegmentIndex: number
/** 当前片段的本地播放时间 */
segmentLocalTime: number
/** 是否已播完 */
isEnded: boolean
/** 是否可以播放(至少有 1 个片段) */
canPlay: boolean
/** 播放 */
play: () => void
/** 暂停 */
pause: () => void
/** 切换播放/暂停 */
togglePlayPause: () => void
/** 跳转到全局时间 */
seekTo: (time: number) => void
/** 每个片段对应的 video 元素 ref 数组 */
videoRefs: React.MutableRefObject<(HTMLVideoElement | null)[]>
}
/**
*
*/
function findSegmentAtTime(
segments: PlaybackSegment[],
globalTime: number,
): { index: number; localTime: number } {
let accumulated = 0
for (let i = 0; i < segments.length; i++) {
const seg = segments[i]
const segDuration = seg.endTime - seg.startTime
if (globalTime < accumulated + segDuration || i === segments.length - 1) {
return { index: i, localTime: seg.startTime + (globalTime - accumulated) }
}
accumulated += segDuration
}
return { index: segments.length - 1, localTime: segments[segments.length - 1].endTime }
}
/**
*
*/
function buildTimeline(segments: PlaybackSegment[]): number[] {
const starts: number[] = []
let acc = 0
for (const seg of segments) {
starts.push(acc)
acc += seg.endTime - seg.startTime
}
return starts
}
/**
* useSegmentScheduler video
*
*
* - <video> ref
* - video src + preload="auto"
* - currentSegmentIndex + display load
* -
*/
export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedulerState {
/** 每个片段对应的 video 元素 ref(由组件 JSX 渲染并绑定) */
const videoRefs = useRef<(HTMLVideoElement | null)[]>([])
const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0)
const [currentSegmentIndex, setCurrentSegmentIndex] = useState(0)
const [isEnded, setIsEnded] = useState(false)
const rafRef = useRef<number>(0)
const isSeekingRef = useRef(false)
// 计算时间线
const timelineStarts = useMemo(() => buildTimeline(segments), [segments])
const totalDuration = useMemo(
() => segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0),
[segments],
)
const canPlay = segments.length > 0
// 当前片段信息
const currentSegment = segments[currentSegmentIndex] || null
const segmentLocalTime = currentSegment
? currentTime - (timelineStarts[currentSegmentIndex] || 0) + currentSegment.startTime
: 0
/**
*
* srcvideo JSX seek +
*/
const switchToSegment = useCallback(
(index: number, seekToLocalTime?: number): Promise<void> => {
return new Promise((resolve) => {
// 暂停当前视频
const prevVideo = videoRefs.current[currentSegmentIndex]
if (prevVideo) prevVideo.pause()
const video = videoRefs.current[index]
if (!video || index >= segments.length) {
resolve()
return
}
const seg = segments[index]
const localTime = seekToLocalTime ?? seg.startTime
// 设置播放位置
video.currentTime = localTime
// 如果已有足够帧数据,直接 resolve
if (video.readyState >= 2) {
setCurrentSegmentIndex(index)
resolve()
return
}
// 等待 canplay 事件
const onCanPlay = () => {
video.removeEventListener("canplay", onCanPlay)
clearTimeout(timeoutId)
setCurrentSegmentIndex(index)
resolve()
}
// 10 秒超时保护
const timeoutId = setTimeout(() => {
video.removeEventListener("canplay", onCanPlay)
console.warn(
`[useSegmentScheduler] 片段 ${index} 预加载超时 (10s), readyState=${video.readyState}`,
)
setCurrentSegmentIndex(index)
resolve()
}, 10000)
video.addEventListener("canplay", onCanPlay)
})
},
[segments, currentSegmentIndex],
)
/** 播放循环 — 检测片段边界并切换 */
const tick = useCallback(() => {
const video = videoRefs.current[currentSegmentIndex]
if (!video || isSeekingRef.current) {
rafRef.current = requestAnimationFrame(tick)
return
}
const seg = segments[currentSegmentIndex]
if (!seg) return
// 检查是否到达出点(容差 0.15s)
if (video.currentTime >= seg.endTime - 0.15) {
video.pause()
const nextIndex = currentSegmentIndex + 1
if (nextIndex < segments.length) {
switchToSegment(nextIndex).then(() => {
setIsPlaying(true)
rafRef.current = requestAnimationFrame(tick)
const nextVideo = videoRefs.current[nextIndex]
if (nextVideo) {
const canPlay = () => {
nextVideo
.play()
.catch((e) =>
console.warn("[useSegmentScheduler] auto-play next segment failed:", e),
)
}
if (nextVideo.readyState >= 3) {
canPlay()
} else {
const timeout = setTimeout(canPlay, 300)
nextVideo.addEventListener(
"canplay",
() => {
clearTimeout(timeout)
canPlay()
},
{ once: true },
)
}
}
})
const accumulatedTime =
(timelineStarts[currentSegmentIndex] || 0) + (seg.endTime - seg.startTime)
setCurrentTime(accumulatedTime)
} else {
setIsPlaying(false)
setIsEnded(true)
setCurrentTime(totalDuration)
return
}
} else {
const globalTime =
(timelineStarts[currentSegmentIndex] || 0) + (video.currentTime - seg.startTime)
setCurrentTime(Math.max(0, Math.min(globalTime, totalDuration)))
}
rafRef.current = requestAnimationFrame(tick)
}, [segments, currentSegmentIndex, timelineStarts, totalDuration, switchToSegment])
/** 播放 */
const play = useCallback(async () => {
if (!canPlay) return
setIsEnded(false)
// 确保第一段可播放
const firstVideo = videoRefs.current[0]
if (firstVideo && currentSegmentIndex === 0 && firstVideo.readyState < 2) {
await switchToSegment(0)
}
const video = videoRefs.current[currentSegmentIndex]
if (!video) return
try {
const playPromise = video.play()
if (playPromise !== undefined) {
await playPromise
}
setIsPlaying(true)
rafRef.current = requestAnimationFrame(tick)
} catch (err) {
console.warn("[useSegmentScheduler] 播放失败:", err)
}
}, [canPlay, switchToSegment, tick, currentSegmentIndex])
/** 暂停 */
const pause = useCallback(() => {
const video = videoRefs.current[currentSegmentIndex]
if (video) video.pause()
setIsPlaying(false)
cancelAnimationFrame(rafRef.current)
}, [currentSegmentIndex])
/** 切换播放/暂停 */
const togglePlayPause = useCallback(() => {
if (isPlaying) {
pause()
} else {
if (isEnded) {
// 播放结束后再次播放,从头开始
setIsEnded(false)
switchToSegment(0, segments[0]?.startTime).then(() => {
const video = videoRefs.current[0]
if (video) {
video.play().catch((e) => console.warn("[useSegmentScheduler] restart play failed:", e))
setIsPlaying(true)
setCurrentTime(0)
rafRef.current = requestAnimationFrame(tick)
}
})
} else {
play()
}
}
}, [isPlaying, isEnded, pause, play, switchToSegment, segments, tick])
/** 跳转到指定全局时间 */
const seekTo = useCallback(
async (time: number) => {
if (!canPlay) return
const clampedTime = Math.max(0, Math.min(time, totalDuration))
const { index, localTime } = findSegmentAtTime(segments, clampedTime)
isSeekingRef.current = true
if (index !== currentSegmentIndex) {
await switchToSegment(index, localTime)
} else {
const video = videoRefs.current[index]
if (video) video.currentTime = localTime
}
setCurrentTime(clampedTime)
setIsEnded(false)
setTimeout(() => {
isSeekingRef.current = false
}, 200)
},
[canPlay, totalDuration, segments, currentSegmentIndex, switchToSegment],
)
// 确保 videoRefs 数组长度与 segments 一致 + 强制预加载
useEffect(() => {
videoRefs.current = videoRefs.current.slice(0, segments.length)
while (videoRefs.current.length < segments.length) {
videoRefs.current.push(null)
}
// 强制预加载:所有 video 元素挂载后,调用 load() 确保浏览器真正开始加载数据
videoRefs.current.forEach((video) => {
if (video) {
video.load()
}
})
}, [segments])
// 组件卸载时清理
useEffect(() => {
return () => {
cancelAnimationFrame(rafRef.current)
}
}, [])
// 片段列表变化时重置
useEffect(() => {
cancelAnimationFrame(rafRef.current)
setIsPlaying(false)
setCurrentTime(0)
setCurrentSegmentIndex(0)
setIsEnded(false)
}, [segments])
return {
isPlaying,
currentTime,
totalDuration,
currentSegmentIndex,
segmentLocalTime,
isEnded,
canPlay,
play,
pause,
togglePlayPause,
seekTo,
videoRefs,
}
}
export default useSegmentScheduler
@@ -1,453 +0,0 @@
/**
* Step 4 Hook + voice_ids
* /generation/preview
*/
import { useState, useCallback, useMemo, useEffect, useRef } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation"
import { updateEditPlan } from "@/api/template-editor/editPlans"
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[]
/** 配音素材库ID(用户选择的上传音频或AI配音素材) */
voiceLibraryId?: 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,
voiceLibraryId,
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,
),
)
// 保存预览视频 URL 到 plan config,供封面生成使用
if (result.videoUrl && selectedTemplate) {
updateEditPlan(selectedTemplate, {
config: { rendered_storage_key: result.videoUrl },
}).catch((err) => {
console.warn("[Step4] 保存预览视频URL到plan config失败:", err)
})
}
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))
},
[selectedTemplate],
)
/** 生成所有预览 */
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,
voice_library_id: voiceLibraryId || 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,
voiceLibraryId,
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
/** 当前选中预览的 taskId(用于确认生成时复用预览产物) */
const selectedTaskId = selectedResult?.taskId ?? ""
return {
templateName,
materialCount,
duration,
videoRatio,
// 多预览状态
items,
selectedIndex,
setSelectedIndex,
previewCount,
// 综合状态
previewStatus,
previewResult: selectedResult,
previewError,
progress,
canProceed,
allReady,
anyReady,
anyGenerating,
generatePreview,
regeneratePreview,
// 确认生成复用预览产物
selectedTaskId,
}
}
export default useStep4Preview
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react"
import { message } from "antd"
import type { CoverConfig, CoverTemplate } from "../types/cover"
import { generateCover } from "@/api/generation"
import { createPreview, getPreviewStatus } from "@/api/generation/preview"
import {
fetchCoverTemplates,
createCoverTemplate,
@@ -109,31 +110,109 @@ export function useStep6Cover({
console.error("[Step6] 智能封面生成失败:", err)
// 提取详细错误信息
let errorMsg = "封面生成失败"
const e = err as {
response?: { data?: { detail?: string; message?: string }; status?: number }
request?: unknown
message?: string
}
if (e.response) {
// 后端返回错误
const detail = e.response.data?.detail || e.response.data?.message || ""
errorMsg = detail || `后端错误 (${e.response.status})`
console.error("[Step6] 后端返回:", e.response.data)
} else if (e.request) {
// 请求已发送但无响应
errorMsg = "服务器无响应,请检查网络连接"
console.error("[Step6] 请求无响应:", e.request)
} else if (e.message) {
errorMsg = e.message
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyErr = err as any
const statusCode = anyErr?.response?.status
message.error(errorMsg)
// 400 错误:精确判断是否为"预览缺失",避免误判其他 400 错误
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errCode = anyErr?.response?.data?.code as string | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errMsg = (anyErr?.response?.data?.message ||
anyErr?.response?.data?.detail ||
"") as string
const isPreviewMissing =
statusCode === 400 &&
(errCode?.includes("PREVIEW") ||
/预览.*(?:缺失|不存在|未找到)|(?:missing|not found|does not exist).*preview/i.test(
errMsg,
))
if (isPreviewMissing) {
console.log("[Step6] 检测到预览缺失,尝试自动创建预览渲染任务...")
message.info("正在准备预览视频,请稍候...")
try {
const previewResp = await createPreview({
template_id: selectedTemplate,
asset_ids: assetIds,
duration: duration || 30,
})
// 轮询等待预览渲染完成:递归 setTimeout 避免请求重叠 + 120s 超时兜底
await new Promise<void>((resolve, reject) => {
let finished = false
const done = (fn: () => void) => {
if (finished) return
finished = true
clearTimeout(timeoutId)
fn()
}
const timeoutId = setTimeout(() => {
done(() => reject(new Error("预览生成超时,请稍后重试")))
}, 120_000)
const poll = async () => {
if (finished) return
try {
const status = await getPreviewStatus(previewResp.task_id)
if (status.status === "completed") {
done(() => resolve())
} else if (status.status === "failed") {
done(() => reject(new Error(status.error_message || "预览渲染失败")))
} else {
setTimeout(poll, 2000)
}
} catch (e) {
done(() => reject(e))
}
}
poll()
})
message.success("预览视频就绪,重新生成封面...")
// 重试封面生成
const retryResp = await generateCover(selectedTemplate, {
asset_ids: assetIds,
cover_type: "ai_frame",
})
const retryUrl = retryResp.cover?.image_url || ""
if (retryUrl) {
onCoverSettingsChange({
...coverSettings,
thumbnail_url: retryUrl,
ai_suggested_time: retryResp.cover?.frame_time ?? null,
})
message.success("封面生成成功")
} else {
message.warning("封面生成未返回图片,请重试")
}
} catch (retryErr) {
console.error("[Step6] 自动创建预览后重试失败:", retryErr)
message.error("预览视频创建失败,请稍后重试")
}
} else if (anyErr?.__msgShown) {
// 拦截器已处理,不再重复弹出
} else {
let errorMsg = "封面生成失败"
const e = anyErr as {
response?: { data?: { detail?: string; message?: string }; status?: number }
request?: unknown
message?: string
}
if (e.response) {
const detail = e.response.data?.detail || e.response.data?.message || ""
errorMsg = detail || `后端错误 (${e.response.status})`
console.error("[Step6] 后端返回:", e.response.data)
} else if (e.request) {
errorMsg = "服务器无响应,请检查网络连接"
console.error("[Step6] 请求无响应:", e.request)
} else if (e.message) {
errorMsg = e.message
}
message.error(errorMsg)
}
} finally {
clearTimeout(timeoutId)
setGenerating(false)
}
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange, generating])
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange, generating, duration])
// ── 模板操作方法 ──
const handleSelectTemplate = useCallback((id: string) => {
@@ -1,7 +1,9 @@
/**
* GeneratePage
*
* (1) (2) (3) (4) (5) (6) (7)
* (1) (2) (3) (4) (5) (6) (7)
*
* V24: previewReady
*/
import { message } from "antd"
import type { TitleSettings } from "../types"
@@ -14,7 +16,7 @@ export interface UseStepNavigationOptions {
selectedMaterials: string[]
smartSelectedIds: string[]
titleSettings: TitleSettings
/** Step4 是否已生成预览 */
/** 预览是否就绪(前端素材已加载) */
previewReady: boolean
}
@@ -49,12 +51,12 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav
return
}
// Step3 配音:配音为可选项,不强制校验,用户可跳过
if (currentStep === 4 && !previewReady) {
message.warning("请先生成剪辑预览")
if (currentStep === 4 && !titleSettings.title.trim()) {
message.warning("请选择或输入标题")
return
}
if (currentStep === 5 && !titleSettings.title.trim()) {
message.warning("请选择或输入标题")
if (currentStep === 5 && !previewReady) {
message.warning("请选择素材以预览效果")
return
}
if (currentStep < 7) {
@@ -0,0 +1,54 @@
/**
*
* GeneratePage FrontendPreviewPlayer
*/
export interface DurationAsset {
id?: string
duration?: number
metadata?: { duration?: number }
}
export interface DurationTemplateSegment {
duration_min?: number
duration_max?: number
}
/**
*
* @param assets
* @param template segments
* @returns 0
*/
export function calculateTotalVideoDuration(
assets: DurationAsset[] | undefined,
template: { segments?: DurationTemplateSegment[] } | undefined,
): number {
if (!assets || assets.length === 0 || !template) return 0
const templateSegments = template.segments || []
return assets.reduce((sum, asset, i) => {
const assetDuration = asset.duration || asset.metadata?.duration || 30
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
const segDuration = tplSeg
? Math.min(
tplSeg.duration_max ?? assetDuration,
Math.max(tplSeg.duration_min ?? 0, assetDuration),
)
: Math.min(assetDuration, 10)
return sum + segDuration
}, 0)
}
/**
* segments
* duration_max
*
*/
export function estimateTotalVideoDuration(
template: { segments?: DurationTemplateSegment[] } | undefined,
): number {
if (!template?.segments || template.segments.length === 0) return 0
return template.segments.reduce((sum, seg) => sum + (seg.duration_max || 0), 0)
}
@@ -2,14 +2,13 @@ import { useState, useCallback } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { message } from "antd"
import {
createAsset,
uploadAssetDirect,
getAssetLibraries,
getIngestJob,
type AssetLibraryItem,
} from "@/api/assets"
import { tagAsset } from "@/api/tags"
import { type VoiceGender, type VoiceMaterial, buildMetadata } from "../../../types"
import { getAudioDuration } from "../../../utils/audio"
import { type VoiceGender, type VoiceMaterial } from "../../../types"
interface UseVoiceUploadOptions {
voiceLibrary?: { id: string; kind: string }
@@ -48,32 +47,34 @@ export function useVoiceUpload({ voiceLibrary, createLibMutation }: UseVoiceUplo
if (!lib) throw new Error("无法创建配音库")
}
// 2. 上传文件(带进度)
const { storage_key } = await uploadAssetDirect({
// 2. 上传文件(带进度,后端自动创建 ingest job
const { ingest_job_id } = await uploadAssetDirect({
file: data.file,
library_id: lib.id,
onProgress: (p) => setUploadProgress(p),
})
// 3. 获取音频时长
const duration = await getAudioDuration(data.file)
// 3. 轮询 ingest job 状态
let job: Awaited<ReturnType<typeof getIngestJob>> | null = null
let retries = 0
const maxRetries = 60 // 最多等待 5 分钟
while (retries < maxRetries) {
await new Promise((r) => setTimeout(r, 5000))
job = await getIngestJob(ingest_job_id)
if (job.status === "completed" || job.status === "failed") break
retries++
}
// 4. 创建素材记录
const asset = await createAsset({
library_id: lib.id,
name: data.name,
storage_key,
mime_type: data.file.type || "audio/mpeg",
metadata: buildMetadata({
gender: data.gender,
description: data.description,
duration,
}),
})
if (!job || job.status === "failed") {
throw new Error("音频处理失败,请重试")
}
if (retries >= maxRetries) {
throw new Error("音频处理超时,请稍后在素材库查看")
}
// 5. 打标签(标签走独立 API
if (data.tagIds.length > 0) {
await tagAsset(asset.id, data.tagIds)
// 4. 打标签(标签走独立 API
if (data.tagIds.length > 0 && job.result_asset_id) {
await tagAsset(job.result_asset_id, data.tagIds)
}
} finally {
setUploadProgress(null)
+1 -1
View File
@@ -21,7 +21,7 @@ export interface VoiceMaterial {
fileUrl?: string
}
/** 配音素材上传元数据(传递给 createAsset 的 metadata */
/** 配音素材上传元数据(上传素材的 metadata */
export interface VoiceAssetMetadata {
gender: VoiceGender
description: string
@@ -1,8 +1,6 @@
import { useState, useCallback } from "react"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { uploadAssetDirect, getAssetLibraries, createAsset } from "@/api/assets"
import { getAudioDuration } from "../utils/audio"
import { buildVoiceMetadata } from "../types"
import { uploadAssetDirect, getAssetLibraries, getIngestJob } from "@/api/assets"
/**
* Hook
@@ -33,27 +31,30 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
const lib = libs.find((l) => l.kind === "voice")
if (!lib) throw new Error("配音库不存在,请先在配音库页面创建")
/* 直传文件 */
const { storage_key } = await uploadAssetDirect({
/* 直传文件(后端会自动创建 ingest job) */
const { ingest_job_id } = await uploadAssetDirect({
file: data.file,
library_id: lib.id,
onProgress: (p) => setUploadProgress(p),
})
/* 获取音频时长 */
const duration = await getAudioDuration(data.file)
/* 轮询 ingest job 状态,等待 Worker 处理完成 */
let jobStatus = ""
let retries = 0
const maxRetries = 60 // 最多等待 5 分钟(60 * 5秒)
while (jobStatus !== "ready" && jobStatus !== "failed" && retries < maxRetries) {
await new Promise((r) => setTimeout(r, 5000))
const job = await getIngestJob(ingest_job_id)
jobStatus = job.status
retries++
}
/* 创建素材记录 */
await createAsset({
library_id: lib.id,
name: data.name,
storage_key,
mime_type: data.file.type || "audio/mpeg",
metadata: buildVoiceMetadata({
description: data.description,
duration,
}),
})
if (jobStatus === "failed") {
throw new Error("音频处理失败,请重试")
}
if (retries >= maxRetries) {
throw new Error("音频处理超时,请稍后在素材库查看")
}
} finally {
setUploadProgress(null)
}
+1 -1
View File
@@ -39,7 +39,7 @@ export interface ClonedVoiceDisplay {
sampleUrl?: string
}
/** 音色上传元数据(传递给 createAsset 的 metadata */
/** 音色上传元数据(上传素材的 metadata */
export interface VoiceUploadMetadata {
gender?: string
description?: string
-34
View File
@@ -7,11 +7,9 @@ import {
deleteAssetLibrary,
getAssets,
getAssetsByKind,
createAsset,
updateAsset,
updateAssetReviewStatus,
deleteAsset,
uploadAsset,
prepareDirectUpload,
completeDirectUpload,
uploadAssetDirect,
@@ -175,22 +173,6 @@ describe("assets API", () => {
})
})
describe("createAsset", () => {
it("should resolve successfully", async () => {
await expect(createAsset({ name: "test-item" })).resolves.not.toThrow()
})
it("should reject on API error", async () => {
mockGet.mockRejectedValue(new Error("Network error"))
mockPost.mockRejectedValue(new Error("Network error"))
mockPut.mockRejectedValue(new Error("Network error"))
mockDelete.mockRejectedValue(new Error("Network error"))
mockPatch.mockRejectedValue(new Error("Network error"))
await expect(createAsset({ name: "test-item" })).rejects.toThrow()
})
})
describe("updateAsset", () => {
it("should resolve successfully", async () => {
await expect(updateAsset("test-assetId")).resolves.not.toThrow()
@@ -239,22 +221,6 @@ describe("assets API", () => {
})
})
describe("uploadAsset", () => {
it("should resolve successfully", async () => {
await expect(uploadAsset(new FormData())).resolves.not.toThrow()
})
it("should reject on API error", async () => {
mockGet.mockRejectedValue(new Error("Network error"))
mockPost.mockRejectedValue(new Error("Network error"))
mockPut.mockRejectedValue(new Error("Network error"))
mockDelete.mockRejectedValue(new Error("Network error"))
mockPatch.mockRejectedValue(new Error("Network error"))
await expect(uploadAsset(new FormData())).rejects.toThrow()
})
})
describe("prepareDirectUpload", () => {
it("should resolve successfully", async () => {
await expect(prepareDirectUpload({ name: "test-item" })).resolves.not.toThrow()
+46 -20
View File
@@ -20,9 +20,16 @@ vi.mock("@/api/auth", () => ({
refreshAccessToken: vi.fn(),
}))
vi.mock("@/api/auth/tokenRefresh", () => ({
scheduleProactiveRefresh: vi.fn(),
cancelProactiveRefresh: vi.fn(),
executeTokenRefresh: vi.fn(),
}))
import { message } from "antd"
import { useAuthStore } from "@/store/authStore"
import { refreshAccessToken } from "@/api/auth"
import { executeTokenRefresh } from "@/api/auth/tokenRefresh"
import apiClient from "@/api/client"
// 从真实实例取出拦截器回调
@@ -264,23 +271,28 @@ describe("apiClient - 401 token refresh", () => {
expect(window.location.href).toBe("/")
})
it("refreshes token on 401 and calls setAuth", async () => {
it("refreshes token on 401 and calls executeTokenRefresh", async () => {
const mockSetAuth = vi.fn()
vi.mocked(useAuthStore.getState).mockReturnValue({
let currentAccessToken = "old-access"
vi.mocked(useAuthStore.getState).mockImplementation(() => ({
user: { id: "1", email: "test@test.com" },
accessToken: "old-access",
accessToken: currentAccessToken,
refreshToken: "old-refresh",
isAuthenticated: true,
clearAuth: vi.fn(),
setAuth: mockSetAuth,
} as any)
vi.mocked(refreshAccessToken).mockResolvedValue({
access_token: "new-access",
refresh_token: "new-refresh",
} as never)
setAuth: ((_user: any, newAccess: string, _newRefresh: string) => {
currentAccessToken = newAccess
mockSetAuth(_user, newAccess, _newRefresh)
}) as any,
}))
// Mock executeTokenRefresh to simulate successful refresh
vi.mocked(executeTokenRefresh).mockImplementation(() => {
currentAccessToken = "new-access"
mockSetAuth({ id: "1", email: "test@test.com" }, "new-access", "new-refresh")
return Promise.resolve()
})
// 拦截器重试时会调用 apiClient(config),会真的发请求,最终会 reject
// 但我们只关心刷新逻辑是否正确执行
const err = makeAxiosError(401, { detail: "Unauthorized" })
try {
@@ -289,31 +301,45 @@ describe("apiClient - 401 token refresh", () => {
// 重试会因为没有真实网络而失败,忽略
}
expect(refreshAccessToken).toHaveBeenCalledWith("old-refresh")
expect(executeTokenRefresh).toHaveBeenCalled()
expect(mockSetAuth).toHaveBeenCalled()
})
it("handles refresh failure by logging out", async () => {
const mockClearAuth = vi.fn()
vi.mocked(useAuthStore.getState).mockReturnValue({
// After executeTokenRefresh fails, it clears auth (sets accessToken to null)
// and redirects to /login. The promise resolves (doesn't reject).
let currentAccessToken: string | null = "old-access"
vi.mocked(useAuthStore.getState).mockImplementation(() => ({
user: { id: "1", email: "test@test.com" },
accessToken: "old-access",
accessToken: currentAccessToken,
refreshToken: "old-refresh",
isAuthenticated: true,
clearAuth: mockClearAuth,
isAuthenticated: currentAccessToken !== null,
clearAuth: (() => {
currentAccessToken = null
mockClearAuth()
window.location.href = "/login"
}) as any,
setAuth: vi.fn(),
} as any)
vi.mocked(refreshAccessToken).mockRejectedValue(new Error("refresh failed") as never)
}))
// Mock executeTokenRefresh: simulates failure → clears auth + redirects
vi.mocked(executeTokenRefresh).mockImplementation(() => {
currentAccessToken = null
mockClearAuth()
window.location.href = "/login"
return Promise.resolve()
})
const err = makeAxiosError(401, { detail: "Unauthorized" })
try {
await responseErrorInterceptor(err)
} catch {
// expected
// expected - rejects because accessToken is null after failed refresh
}
expect(executeTokenRefresh).toHaveBeenCalled()
expect(mockClearAuth).toHaveBeenCalled()
expect(window.location.href).toBe("/")
expect(window.location.href).toBe("/login")
})
})
@@ -10,7 +10,9 @@ vi.mock("@/api/voice-clone", () => ({
}))
vi.mock("@/api/assets", () => ({
uploadAsset: vi.fn(),
uploadAssetDirect: vi
.fn()
.mockResolvedValue({ storage_key: "test", ingest_job_id: "test", url: "http://test" }),
}))
vi.mock("@/components/ui", () => ({
+21 -8
View File
@@ -31,14 +31,27 @@ vi.mock("react-router-dom", async () => {
})
vi.mock("@/store/authStore", () => ({
useAuthStore: (selector: any) =>
selector({
user: { id: "1", username: "testuser" },
token: "mock-token",
isAuthenticated: true,
setAuth: mockSetAuth,
clearAuth: mockClearAuth,
}),
useAuthStore: Object.assign(
(selector: any) =>
selector({
user: { id: "1", username: "testuser" },
token: "mock-token",
refreshToken: "mock-refresh-token",
isAuthenticated: true,
setAuth: mockSetAuth,
clearAuth: mockClearAuth,
}),
{
getState: () => ({
user: { id: "1", username: "testuser" },
token: "mock-token",
refreshToken: "mock-refresh-token",
isAuthenticated: true,
setAuth: mockSetAuth,
clearAuth: mockClearAuth,
}),
},
),
}))
vi.mock("@tanstack/react-query", () => ({
@@ -180,7 +180,6 @@ vi.mock("@/api/assets", () => ({
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({}),
uploadAssetDirect: vi.fn().mockResolvedValue({}),
@@ -165,7 +165,6 @@ vi.mock("@/api/assets", () => ({
deleteAssetLibrary: vi.fn().mockResolvedValue({}),
getAssetsByKind: vi.fn().mockResolvedValue({ items: [], total: 0 }),
getAssets: vi.fn().mockResolvedValue({ items: [], total: 0 }),
createAsset: vi.fn().mockResolvedValue({}),
updateAsset: vi.fn().mockResolvedValue({}),
deleteAsset: vi.fn().mockResolvedValue({}),
uploadAssetDirect: vi.fn().mockResolvedValue({}),
@@ -17,7 +17,7 @@ import "@/api/generation/types"
// 直接引入所有 Step 组件,建立完整依赖链
import "@/pages/generate/GeneratePage"
import "@/pages/generate/components/Step2MaterialSelect"
import "@/pages/generate/components/Step4GeneratePreview"
import "@/pages/generate/components/Step5GeneratePreview"
import "@/pages/generate/components/Step4TitleSettings"
import "@/pages/generate/components/Step5VoiceSelect"
import "@/pages/generate/components/PreviewVideoPanel"
@@ -47,7 +47,8 @@ describe("GeneratePage module smoke test", () => {
})
})
import "@/pages/generate/hooks/useGenerateVideo"
import "@/pages/generate/hooks/useStep4Preview"
import "@/pages/generate/hooks/usePreviewAssets"
import "@/pages/generate/hooks/useSegmentScheduler"
import "@/pages/generate/hooks/generate-video/useGenerationPolling"
import "@/pages/generate/hooks/useGenerateFormState"
import "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection"
@@ -1,17 +0,0 @@
/**
* 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)
})
})
@@ -0,0 +1,19 @@
/**
* Step5GeneratePreview smoke test
* vitest related 5
*/
import { describe, it, expect } from "vitest"
import "@/pages/generate/components/Step5GeneratePreview"
import "@/pages/generate/hooks/usePreviewAssets"
import "@/pages/generate/hooks/useSegmentScheduler"
import "@/pages/generate/components/FrontendPreviewPlayer"
import "@/pages/generate/hooks/useStepNavigation"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/GeneratePage"
describe("Step5GeneratePreview module smoke test", () => {
it("should load all step5 preview modules", () => {
expect(true).toBe(true)
})
})
@@ -93,5 +93,5 @@ describe("useStep5Voice smoke test", () => {
)
expect(result.current).toBeDefined()
expect(typeof result.current.handlePlayCloneSample).toBe("function")
})
}, 15_000)
})
+1
View File
@@ -13,6 +13,7 @@ export default defineConfig({
environment: "jsdom",
globals: true,
setupFiles: ["./src/test/setup.ts"],
testTimeout: 15_000, // 全局 15 秒,防止 CI 高负载时偶发超时
},
plugins: [
react({
+2 -12
View File
@@ -85,19 +85,9 @@ def create_video_record_and_dedup(
if thumbnail_url:
generated_video.thumbnail_url = thumbnail_url
video_repo.update_thumbnail(video_id, thumbnail_url)
logger.info("Thumbnail reused (pre-generated) for video %s", video_id)
logger.info("Thumbnail set for video %s: %s", video_id, thumbnail_url[:80] if thumbnail_url else "")
else:
thumbnail_storage_key = f"generated/projects/{project_id}/thumbnails/{video_id}.jpg"
try:
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
_thumbnail_url = generate_and_upload_thumbnail(video_path, thumbnail_storage_key)
if _thumbnail_url:
generated_video.thumbnail_url = _thumbnail_url
video_repo.update_thumbnail(video_id, _thumbnail_url)
logger.info("Thumbnail generated for video %s: %s", video_id, _thumbnail_url)
except Exception as thumb_err:
logger.warning("Thumbnail generation failed for %s: %s", video_id, thumb_err)
logger.debug("No thumbnail_url provided for video %s, skipping", video_id)
# 计算视频指纹
deduplicator = VideoDeduplicator()
+4 -4
View File
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
# OSS 上传配置
OSS_CONNECT_TIMEOUT = 10 # 连接超时(秒),防止 TCP 握手挂死
OSS_UPLOAD_TOTAL_TIMEOUT = 300 # 单文件上传总超时(秒),防止网络慢时无限卡住
OSS_UPLOAD_TOTAL_TIMEOUT = 900 # 单文件上传总超时(秒),防止网络慢时无限卡住
OSS_MULTIPART_THRESHOLD = 100 * 1024 * 1024 # 分片上传阈值:100MB 以上走分片
OSS_PART_SIZE = 8 * 1024 * 1024 # 分片大小:8MB
OSS_MULTIPART_NUM_THREADS = 3 # 分片上传并发数
@@ -127,10 +127,10 @@ def download_asset(asset_storage_key: str, local_path: Path) -> bool:
def _download_via_http(url: str, local_path: Path) -> bool:
"""通过 HTTP 下载文件(支持预签名 URL)。
使用流式下载避免大文件内存溢出超时 300s
使用流式下载避免大文件内存溢出超时 900s
"""
try:
resp = requests.get(url, stream=True, timeout=300)
resp = requests.get(url, stream=True, timeout=900)
resp.raise_for_status()
with open(local_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8 * 1024 * 1024):
@@ -146,7 +146,7 @@ def upload_to_oss(local_path: Path | str, storage_key: str) -> str | None:
"""上传文件到 OSS,返回公开 URL。
大文件>100MB自动走分片上传降低内存峰值减少 OOM 风险
上传加总超时保护默认 300s防止网络异常时无限挂死
上传加总超时保护默认 900s防止网络异常时无限挂死
Args:
local_path: 本地文件路径Path str 均可
+19 -2
View File
@@ -47,7 +47,14 @@ def _parse_resolution(resolution_str: str | None) -> tuple[int, int]:
w, h = resolution_str.lower().split("x", 1)
width = int(w.strip())
height = int(h.strip())
if width <= 0 or height <= 0:
# 最小 100px 防护:避免前端传入宽高比(如 "9:16")被 parseInt 截断为极小值
if width < 100 or height < 100:
logger.warning(
"分辨率异常小 (%dx%d),使用默认值。原始值: %s",
width,
height,
resolution_str,
)
return DEFAULT_OUTPUT_WIDTH, DEFAULT_OUTPUT_HEIGHT
return width, height
except (ValueError, TypeError):
@@ -512,6 +519,8 @@ class RenderAdapter:
# 3. 读取输出分辨率
export_config = plan_config.get("export", {}) or {}
if not isinstance(export_config, dict):
export_config = {}
output_width, output_height = _parse_resolution(export_config.get("resolution"))
logger.info(
"渲染输出分辨率: plan_id=%s resolution=%dx%d source=%s",
@@ -576,7 +585,15 @@ class RenderAdapter:
try:
from video_processing.thumbnail_generator import extract_and_upload_cover_frames
cover_candidates = extract_and_upload_cover_frames(str(result.output_path), plan_id, num_frames=3)
# 从 plan config 提取标题文字,叠加到封面候选帧上
_title_cfg = (plan_config or {}).get("title", {}) or {}
if not isinstance(_title_cfg, dict):
_title_cfg = {}
_title_text = (_title_cfg.get("text", "") or "").strip() if _title_cfg.get("enabled", True) else ""
cover_candidates = extract_and_upload_cover_frames(
str(result.output_path), plan_id, num_frames=3, title_text=_title_text
)
if cover_candidates:
logger.info(
"[render-adapter] 封面候选帧生成成功: plan_id=%s count=%d",
+3 -3
View File
@@ -131,7 +131,7 @@ def mix_audio(
if not main_clips and not audio_clips:
# 没有主音频也没有独立音频 → 检查是否有 BGM
if bgm_path and bgm_config and bgm_config.get("enabled", False):
if bgm_path and bgm_config and isinstance(bgm_config, dict) and bgm_config.get("enabled", False):
from video_processing.bgm_mixer import BGMConfig, build_bgm_only
bgm_cfg = BGMConfig.from_config_dict(bgm_path, bgm_config)
@@ -161,7 +161,7 @@ def mix_audio(
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):
if bgm_path and bgm_config and isinstance(bgm_config, dict) and bgm_config.get("enabled", False):
from video_processing.bgm_mixer import BGMConfig, mix_bgm_with_main
bgm_cfg = BGMConfig.from_config_dict(bgm_path, bgm_config)
@@ -174,7 +174,7 @@ def mix_audio(
logger.exception("[bgm] BGM 混音失败,回退到无 BGM 音频: plan_id=%s", ctx.plan_id)
# ── 多轨道混音(配音/音效等) ──
if audio_tracks_config and audio_tracks_config.get("enabled", False):
if audio_tracks_config and isinstance(audio_tracks_config, dict) and audio_tracks_config.get("enabled", False):
from video_processing.multi_track_mixer import mix_audio_tracks_from_config
try:
@@ -33,7 +33,7 @@ class ReverseConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "ReverseConfig":
"""从字典解析配置."""
if not data:
if not isinstance(data, dict):
return cls(enabled=False)
try:
if not data.get("enabled", False):
@@ -13,6 +13,16 @@ import logging
from pathlib import Path
from typing import Any
from packages.domain.ass_subtitle_builder import (
TITLE_MARGIN_SIDE,
TITLE_MARGIN_TOP,
_wrap_title_text,
build_ass_style,
escape_ass_text,
format_ass_time,
hex_to_ass_color,
position_to_ass_alignment,
)
from packages.domain.subtitle import SubtitleTimeline
logger = logging.getLogger(__name__)
@@ -100,7 +110,10 @@ def generate_ass_from_timeline(
*,
video_width: int,
video_height: int,
video_duration: float = 0.0,
subtitle_config: dict[str, Any] | None = None,
title_text: str = "",
title_config: dict[str, Any] | None = None,
) -> Path:
"""从字幕时间轴生成 ASS 字幕文件。
@@ -160,7 +173,76 @@ def generate_ass_from_timeline(
events.append(f"Dialogue: 0,{start_time},{end_time},Default,,0,0,0,,{safe_text}")
# 组装 ASS 文件
# ── 标题样式与事件(叠加在 ASR 字幕之上)───────────────────────────
title_cfg = title_config or {}
if not isinstance(title_cfg, dict):
title_cfg = {}
title_enabled = title_cfg.get("enabled", True) and bool(title_text.strip())
title_style_line = ""
title_event_line = ""
if title_enabled:
# 兼容 boolean stroke/shadow → dict
_stroke_val = title_cfg.get("stroke")
if isinstance(_stroke_val, bool):
title_cfg["stroke"] = (
{"enabled": _stroke_val, "color": "#000000", "width": 2} if _stroke_val else {"enabled": False}
)
_shadow_val = title_cfg.get("shadow")
if isinstance(_shadow_val, bool):
title_cfg["shadow"] = (
{"enabled": _shadow_val, "color": "#000000", "blur": 4, "offset_x": 2, "offset_y": 2}
if _shadow_val
else {"enabled": False}
)
# 字段名归一化: font_size→size, font_color→color
if "font_size" in title_cfg and "size" not in title_cfg:
title_cfg["size"] = title_cfg["font_size"]
if "font_color" in title_cfg and "color" not in title_cfg:
title_cfg["color"] = title_cfg["font_color"]
t_color = hex_to_ass_color(title_cfg.get("color", "#ffffff"))
t_stroke = title_cfg.get("stroke", {}) or {}
t_shadow = title_cfg.get("shadow", {}) or {}
s_color = hex_to_ass_color(t_stroke.get("color", "#000000"))
s_width = float(t_stroke.get("width", 2)) if t_stroke.get("enabled", False) else 0.0
sh_blur = float(t_shadow.get("blur", 4)) if t_shadow.get("enabled", False) else 0.0
sh_offset = (
t_shadow.get("offset_x", 2) if t_shadow.get("enabled", False) else 0,
t_shadow.get("offset_y", 2) if t_shadow.get("enabled", False) else 0,
)
t_alignment = position_to_ass_alignment(title_cfg.get("position", "top"))
title_style_line = build_ass_style(
"TitleStyle",
font_name=title_cfg.get("font", "思源黑体"),
font_size=min(int(title_cfg.get("size", 36)), 36),
primary_color=t_color,
outline_color=s_color,
outline_width=s_width,
shadow_blur=sh_blur,
shadow_offset=sh_offset,
bold=bool(title_cfg.get("bold", True)),
italic=bool(title_cfg.get("italic", False)),
alignment=t_alignment,
margin_v=TITLE_MARGIN_TOP,
margin_l=TITLE_MARGIN_SIDE,
margin_r=TITLE_MARGIN_SIDE,
)
t_font_size = min(int(title_cfg.get("size", 36)), 36)
safe_raw = escape_ass_text(title_text.strip())
safe_wrapped = _wrap_title_text(safe_raw, video_width, t_font_size)
if video_duration > 0:
t_end_time = format_ass_time(video_duration)
else:
t_end_time = format_ass_time((timeline.segments[-1].end + 5.0) if timeline.segments else 60.0)
title_event_line = f"Dialogue: 0,0:00:00.00,{t_end_time},TitleStyle,,0,0,0,,{safe_wrapped}"
# 组装 ASS 文件
ass_content = f"""[Script Info]
ScriptType: v4.00+
PlayResX: {video_width}
@@ -170,12 +252,12 @@ WrapStyle: 2
Encoding: UTF-8
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
{style_line}
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding # noqa: E501
{chr(10).join(filter(None, [title_style_line, style_line]))}
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
{chr(10).join(events)}
{chr(10).join(filter(None, [title_event_line] + events))}
"""
output_path.parent.mkdir(parents=True, exist_ok=True)
@@ -1,4 +1,8 @@
"""视频缩略图生成工具 — 抽取首帧上传到 OSS。"""
"""视频封面抽帧工具 — 从已渲染视频中抽取帧作为封面。
统一封面管道视频渲染时标题已通过 ASS 字幕烧进视频
渲染完成后直接从此视频抽帧封面天然带标题无需额外叠加逻辑
"""
from __future__ import annotations
@@ -13,28 +17,30 @@ def extract_first_frame(
video_path: str,
output_path: str | None = None,
*,
width: int = 640,
width: int = -1,
height: int = -1,
timeout: int = 30,
seek_ratio: float = 0.15,
min_seek_seconds: float = 1.0,
) -> str:
"""抽取视频封面(默认取视频时长 15% 处的帧,避开片头纯色画面)。
"""抽取视频封面(默认取视频时长 15% 处的帧,避开片头纯色画面)。
因为视频渲染时标题已通过 ASS 字幕烧录抽取的帧天然带标题
Args:
video_path: 视频文件路径
output_path: 输出图片路径不传则用临时文件
width: 输出宽度默认 640-1 表示按比例缩放
height: 输出高度默认 -1按比例缩放
width: 输出宽度默认 -1保持原始分辨率
height: 输出高度默认 -1保持原始分辨率
timeout: 超时时间
seek_ratio: 抽帧位置占视频时长的比例默认 0.15 15%
min_seek_seconds: 最小抽帧时间避免极短视频 seek 0
Returns:
生成的缩略图文件路径
生成的封面帧文件路径
Raises:
subprocess.CalledProcessError: ffmpeg 执行失败
RuntimeError: ffmpeg 执行失败或输出文件为空
"""
from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg
@@ -57,10 +63,20 @@ def extract_first_frame(
# 格式化为 HH:MM:SS.xx
seek_str = _format_seek_time(seek_time)
# -ss 放在 -i 前面(input seeking,更快但精度稍低,缩略图够用)
# 构建 scale filter:如果指定了宽高则缩放,否则保持原始分辨率。
# NOTE: scale_filter 在此处通过 if/else 分支赋值,之后不再被覆盖,
# 后续 cmd / cmd2 均复用同一变量,逻辑无变化。
if width > 0 or height > 0:
w_str = str(width) if width > 0 else "-1"
h_str = str(height) if height > 0 else "-1"
scale_filter = f"scale={w_str}:{h_str}:force_original_aspect_ratio=decrease,format=yuvj420p"
else:
# 保持原始分辨率,只确保格式兼容
scale_filter = "format=yuvj420p"
# -ss 放在 -i 前面(input seeking,更快)
# -vframes 1 只取一帧
# -q:v 2 jpeg 高质量
scale_filter = f"scale={width}:{height}:force_original_aspect_ratio=decrease,format=yuvj420p"
cmd = [
FFMPEG_BIN,
"-y",
@@ -99,7 +115,7 @@ def extract_first_frame(
run_ffmpeg(cmd2, capture_output=True, timeout=timeout)
if not Path(output_path).exists() or Path(output_path).stat().st_size == 0:
raise RuntimeError(f"Thumbnail generation failed: {output_path}")
raise RuntimeError(f"Cover frame extraction failed: {output_path}")
return output_path
except Exception:
@@ -123,166 +139,83 @@ def _format_seek_time(seconds: float) -> str:
def generate_and_upload_thumbnail(
video_path: str,
storage_key: str,
) -> str | None:
"""生成缩略图并上传到 OSS,返回 URL。
Args:
video_path: 本地视频路径
storage_key: OSS 存储 key generated/projects/xxx/thumbnails/yyy.jpg
Returns:
上传成功返回 URL失败返回 None
"""
thumbnail_path = None
try:
thumbnail_path = extract_first_frame(video_path)
except Exception as e:
logger.warning("Failed to extract thumbnail from %s: %s", video_path, e)
return None
try:
from video_processing.oss_helpers import upload_to_oss
url = upload_to_oss(thumbnail_path, storage_key)
return url
except Exception as e:
logger.warning("Failed to upload thumbnail to OSS: %s", e)
return None
finally:
# 清理临时文件
if thumbnail_path:
try:
Path(thumbnail_path).unlink(missing_ok=True)
except Exception:
pass
def extract_cover_candidates(
video_path: str,
num_frames: int = 3,
*,
width: int = 640,
timeout: int = 30,
) -> list[dict]:
"""在视频时长 25%/50%/75% 处各抽一帧,返回候选帧信息列表。
seek_ratio: float = 0.15,
) -> str:
"""从视频中提取一帧缩略图并上传到 OSS。
Args:
video_path: 视频文件路径
num_frames: 抽帧数量默认 3
width: 输出宽度
timeout: 单帧超时
storage_key: OSS 存储 key
seek_ratio: 抽帧位置比例默认 0.15
Returns:
[{"local_path": "...", "frame_time": 5.0}, ...]
上传后的 URL 字符串
Raises:
RuntimeError: 抽帧或上传失败
"""
from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg
from video_processing.oss_helpers import upload_to_oss
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
try:
frame_path = extract_first_frame(video_path, output_path=tmp.name, seek_ratio=seek_ratio)
url = upload_to_oss(frame_path, storage_key)
if not url:
raise RuntimeError(f"上传缩略图到 OSS 失败: {storage_key}")
return url
finally:
Path(tmp.name).unlink(missing_ok=True)
def extract_and_upload_cover_frames(
video_path: str,
plan_id: str,
*,
num_frames: int = 3,
title_text: str = "",
) -> list[dict]:
"""从视频中抽取多帧作为封面候选,上传到 OSS。
Args:
video_path: 视频文件路径
plan_id: 编辑计划 ID用于生成 storage key
num_frames: 抽取帧数默认 3
title_text: 标题文字当前版本未叠加预留参数
Returns:
封面候选列表每项包含 {"url": str, "position": float}
"""
from video_processing.ffmpeg_utils import probe_duration
from video_processing.oss_helpers import upload_to_oss
try:
duration = probe_duration(video_path)
except Exception:
duration = 0.0
if duration <= 0:
duration = 5.0 # fallback
# 计算抽帧时间点:25%, 50%, 75%
ratios = []
for i in range(1, num_frames + 1):
ratios.append(i / (num_frames + 1))
results = []
for _idx, ratio in enumerate(ratios):
frame_time = max(0.5, duration * ratio)
candidates: list[dict] = []
# 均匀分布抽帧点:从 10% 到 90%
for i in range(num_frames):
ratio = 0.1 + 0.8 * i / max(num_frames - 1, 1)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
output_path = tmp.name
try:
seek_str = _format_seek_time(frame_time)
scale_filter = f"scale={width}:-1:force_original_aspect_ratio=decrease,format=yuvj420p"
cmd = [
FFMPEG_BIN,
"-y",
"-ss",
seek_str,
"-i",
frame_path = extract_first_frame(
video_path,
"-vframes",
"1",
"-vf",
scale_filter,
"-q:v",
"2",
output_path,
]
run_ffmpeg(cmd, capture_output=True, timeout=timeout)
if Path(output_path).exists() and Path(output_path).stat().st_size > 0:
results.append(
{
"local_path": output_path,
"frame_time": round(frame_time, 2),
}
)
else:
Path(output_path).unlink(missing_ok=True)
except Exception as e:
logger.warning("封面候选帧抽取失败 ratio=%.2f: %s", ratio, e)
Path(output_path).unlink(missing_ok=True)
return results
def extract_and_upload_cover_frames(
video_path: str,
plan_id: str,
num_frames: int = 3,
) -> list[dict]:
"""抽取封面候选帧并上传到 OSS。
Args:
video_path: 本地视频路径
plan_id: 剪辑计划 ID用于 OSS 路径
num_frames: 抽帧数量
Returns:
[{"image_url": "https://...", "frame_time": 5.0, "storage_key": "covers/xxx/frame_0.jpg"}, ...]
"""
candidates = extract_cover_candidates(video_path, num_frames=num_frames)
if not candidates:
logger.warning("封面候选帧抽取为空: plan_id=%s", plan_id)
return []
results = []
for idx, cand in enumerate(candidates):
local_path = cand["local_path"]
frame_time = cand["frame_time"]
storage_key = f"covers/{plan_id}/frame_{idx}.jpg"
try:
from video_processing.oss_helpers import upload_to_oss
url = upload_to_oss(local_path, storage_key)
output_path=tmp.name,
seek_ratio=ratio,
min_seek_seconds=0.5,
)
storage_key = f"covers/{plan_id}/frame_{i}.jpg"
url = upload_to_oss(frame_path, storage_key)
if url:
results.append(
{
"image_url": url,
"frame_time": frame_time,
"storage_key": storage_key,
}
)
logger.info(
"封面候选帧上传成功: plan_id=%s idx=%d frame_time=%.2f",
plan_id,
idx,
frame_time,
)
seek_time = max(0.5, duration * ratio) if duration > 0 else 0.0
candidates.append({"url": url, "position": round(seek_time, 2)})
except Exception as e:
logger.warning("封面候选帧上传失败: plan_id=%s idx=%d error=%s", plan_id, idx, e)
logger.warning("[thumbnail] 封面候选帧 %d 提取失败: %s", i, e)
finally:
try:
Path(local_path).unlink(missing_ok=True)
except Exception:
pass
Path(tmp.name).unlink(missing_ok=True)
return results
return candidates
@@ -324,6 +324,8 @@ class UnifiedRenderService:
else:
config = self.plan.config or {}
bgm_config = config.get("bgm", {}) or {}
if not isinstance(bgm_config, dict):
bgm_config = {}
audio_tracks_config = config.get("audio_tracks") or {}
noise_reduction_config = config.get("audio_noise_reduction")
ctx = RenderContext(
@@ -480,7 +482,11 @@ class UnifiedRenderService:
"""
config = self.plan.config or {}
title_cfg = config.get("title", {}) or {}
if not isinstance(title_cfg, dict):
title_cfg = {}
subtitle_cfg = config.get("subtitle", {}) or {}
if not isinstance(subtitle_cfg, dict):
subtitle_cfg = {}
title_enabled = title_cfg.get("enabled", True)
subtitle_enabled = subtitle_cfg.get("enabled", True)
@@ -507,7 +513,10 @@ class UnifiedRenderService:
timeline,
video_width=self.output_width,
video_height=self.output_height,
video_duration=video_duration,
subtitle_config=subtitle_cfg,
title_text=title_text,
title_config=title_cfg,
)
logger.info(
"ASR自动字幕生成完成: plan_id=%s segments=%d duration=%.1fs",
@@ -653,7 +662,11 @@ class UnifiedRenderService:
"""
config = self.plan.config or {}
tts_cfg = config.get("tts", {}) or {}
if not isinstance(tts_cfg, dict):
tts_cfg = {}
subtitle_cfg = config.get("subtitle", {}) or {}
if not isinstance(subtitle_cfg, dict):
subtitle_cfg = {}
use_subtitle_align = False # 是否使用字幕对齐模式
# 兼容前端顶层字段:voice_id / custom_text / voice_clone_profile_id
@@ -98,6 +98,7 @@ def _finalize_render_success(
output_path: Path,
engine: str,
thumbnail_url: str = "",
cover_candidates: list[dict] | None = None,
) -> dict:
"""渲染成功后的统一收尾:查重 + 更新状态 + 返回结果。"""
# 创建 GeneratedVideo 记录 + 查重
@@ -107,6 +108,8 @@ def _finalize_render_success(
# 从 plan.config.title.text 读取视频名称
plan_config = plan.config or {}
title_cfg = plan_config.get("title", {}) or {}
if not isinstance(title_cfg, dict):
title_cfg = {}
video_name = (title_cfg.get("text") or "").strip() or f"generated-{generation_task_id[:8]}.mp4"
if generation_task_id:
try:
@@ -161,6 +164,19 @@ def _finalize_render_success(
clip_count=len(rendered_clip_ids),
)
gen_task.completed_at = datetime.now(timezone.utc)
# 回写封面 URL 到 GenerationTask,供封面生成接口读取
if cover_candidates:
first_cover = cover_candidates[0].get("image_url") or cover_candidates[0].get("url") or ""
if first_cover:
gen_task.cover_url = first_cover
logger.info(
"预览渲染完成,回写 cover_url: plan_id=%s task_id=%s url=%s",
plan_id,
generation_task_id,
first_cover[:80],
)
gen_task_repo.update(gen_task)
logger.info(
@@ -286,6 +302,7 @@ def _render_with_unified(
output_path=output_path,
engine="unified",
thumbnail_url=thumbnail_url,
cover_candidates=result.cover_candidates,
)
+87 -161
View File
@@ -13,8 +13,8 @@
from __future__ import annotations
import json
import logging
import os
import tempfile
import time
from pathlib import Path
@@ -36,8 +36,6 @@ from packages.domain.bgm_utils import merge_bgm_config
OUTPUT_WIDTH = 1280
OUTPUT_HEIGHT = 720
OUTPUT_FPS = 25.0
OUTPUT_DURATION_SECONDS = 5.0
GENERATED_FILES_DIR = Path(os.getenv("GENERATED_FILES_DIR", "/app/generated"))
logger = logging.getLogger(__name__)
@@ -154,7 +152,6 @@ def _flush_logs(task_id: str, gen_task) -> None:
# ── 共享工具模块导入 ──────────────────────────────────────────────────────────
from video_processing.dedup_helpers import create_video_record_and_dedup
from video_processing.ffmpeg_utils import FFMPEG_BIN, run_ffmpeg
from video_processing.oss_helpers import (
download_asset,
get_signed_download_url,
@@ -329,61 +326,6 @@ def _build_plan_and_clips_from_task(
return plan, clips, asset_path_map
def _create_fallback_clip(output_path: Path, title: str) -> None:
"""创建 fallback 视频(无素材时)"""
safe_title = title.replace(":", "\\:").replace("'", "\\'")[:80]
run_ffmpeg(
[
FFMPEG_BIN,
"-y",
"-f",
"lavfi",
"-i",
f"color=c=#111827:s={OUTPUT_WIDTH}x{OUTPUT_HEIGHT}:d={OUTPUT_DURATION_SECONDS}:r={int(OUTPUT_FPS)}",
"-vf",
f"drawtext=text='{safe_title}':fontcolor=white:fontsize=48:x=(w-text_w)/2:y=(h-text_h)/2",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
str(output_path),
]
)
def _mux_audio_track(video_path: Path, audio_path: str, output_path: Path) -> None:
"""将音频轨混入已渲染的视频(后处理步骤)。
使用 FFmpeg 将视频和音频合并视频时长为准音频不足则循环
音频过长则截断
"""
command = [
FFMPEG_BIN,
"-y",
"-i",
str(video_path),
"-i",
audio_path,
"-c:v",
"copy",
"-c:a",
"aac",
"-b:a",
"192k",
"-shortest",
"-map",
"0:v:0",
"-map",
"1:a:0",
"-movflags",
"+faststart",
str(output_path),
]
run_ffmpeg(command)
def _download_voice_asset(voice_library_id: str, local_path: Path) -> bool:
"""下载配音文件。
@@ -427,105 +369,6 @@ def _download_voice_asset(voice_library_id: str, local_path: Path) -> bool:
return download_asset(storage_key, local_path)
def _prepare_bgm_track(
*,
bgm_config: dict,
temp_path: Path,
task_id: str = "",
) -> str | None:
"""准备 BGM 音频文件(下载到本地).
支持 3 种来源按优先级
1. audio_url 外部直链 URL最高优先级
2. asset_id 素材库中的音频素材
3. preset_id 预设 BGM
Returns:
BGM 本地文件路径准备失败返回 None
"""
from urllib.parse import urlparse
audio_url = bgm_config.get("audio_url", "") or ""
asset_id = bgm_config.get("asset_id", "") or ""
preset_id = bgm_config.get("preset_id", "") or ""
bgm_file = temp_path / f"bgm_{task_id or 'track'}.mp3"
# 优先级1:外部直链 URL
if audio_url:
try:
parsed = urlparse(audio_url)
if parsed.scheme in ("http", "https"):
from video_processing.url_security import (
ALLOWED_AUDIO_MIME_TYPES,
safe_download_file,
)
logger.info("[task_id=%s] [BGM] 从URL下载: %s", task_id, audio_url[:80])
safe_download_file(
audio_url,
str(bgm_file),
purpose="bgm_download",
allowed_mime_types=ALLOWED_AUDIO_MIME_TYPES,
timeout=60.0,
)
if bgm_file.exists() and bgm_file.stat().st_size > 0:
return str(bgm_file)
except Exception as e:
logger.warning("[task_id=%s] [BGM] URL下载失败: %s", task_id, e)
# 优先级2:素材库素材
if asset_id:
try:
from app.core.db import SessionLocal
from packages.adapters.sqlalchemy_impl.models import AssetModel
session = SessionLocal()
try:
model = session.query(AssetModel).filter(AssetModel.id == asset_id).first()
if model and (model.storage_key or model.file_url):
# 兼容存量数据:storage_key 为空时 fallback 到 file_url
storage_key = model.storage_key or model.file_url
logger.info("[task_id=%s] [BGM] 从素材库下载: asset_id=%s", task_id, asset_id)
ok = download_asset(storage_key, bgm_file)
if ok and bgm_file.exists() and bgm_file.stat().st_size > 0:
return str(bgm_file)
finally:
session.close()
except Exception as e:
logger.warning("[task_id=%s] [BGM] 素材库下载失败: %s", task_id, e)
# 优先级3:预设 BGM 库
if preset_id:
try:
from packages.domain.preset_bgm import get_preset_bgm
preset = get_preset_bgm(preset_id)
if preset and preset.audio_url:
from video_processing.url_security import (
ALLOWED_AUDIO_MIME_TYPES,
safe_download_file,
)
logger.info("[task_id=%s] [BGM] 从预设库下载: preset_id=%s", task_id, preset_id)
safe_download_file(
preset.audio_url,
str(bgm_file),
purpose="bgm_preset_download",
allowed_mime_types=ALLOWED_AUDIO_MIME_TYPES,
timeout=60.0,
)
if bgm_file.exists() and bgm_file.stat().st_size > 0:
return str(bgm_file)
except Exception as e:
logger.warning("[task_id=%s] [BGM] 预设库下载失败: %s", task_id, e)
# 所有来源都失败
logger.warning("[task_id=%s] [BGM] 所有来源都无法获取BGM,跳过", task_id)
return None
def _verify_url_accessible(
url: str,
timeout: float = 10.0,
@@ -983,9 +826,9 @@ def _load_template_plan_config(template_id: str) -> dict:
# 从独立字段组装成 plan.config 格式
plan_config: dict[str, Any] = {}
title_cfg = template.title_config or {}
subtitle_cfg = template.subtitle_config or {}
bgm_cfg = template.bgm_config or {}
title_cfg = template.title_config if isinstance(template.title_config, dict) else {}
subtitle_cfg = template.subtitle_config if isinstance(template.subtitle_config, dict) else {}
bgm_cfg = template.bgm_config if isinstance(template.bgm_config, dict) else {}
if title_cfg:
plan_config["title"] = title_cfg
@@ -1123,6 +966,7 @@ def _render_video(
resolution: str = "",
bgm_config: dict | None = None,
voice_ids: list[str] | None = None,
custom_title: str = "",
) -> tuple[Path, float]:
"""渲染视频(含配音混音)。
@@ -1155,10 +999,33 @@ def _render_video(
list(template_config.keys()),
)
# ── 用户自定义标题覆盖模板标题配置 ──────────────────────────────────
if custom_title:
try:
user_title_cfg = json.loads(custom_title) if isinstance(custom_title, str) else custom_title
if isinstance(user_title_cfg, dict) and user_title_cfg.get("text", "").strip():
# 字段名归一化: 前端 font_size/font_color → 后端 size/color
if "font_size" in user_title_cfg and "size" not in user_title_cfg:
user_title_cfg["size"] = user_title_cfg["font_size"]
if "font_color" in user_title_cfg and "color" not in user_title_cfg:
user_title_cfg["color"] = user_title_cfg["font_color"]
plan_cfg = dict(virtual_plan.config or {})
plan_cfg["title"] = user_title_cfg
virtual_plan.config = plan_cfg
logger.info(
"[task_id=%s] [渲染] 用户自定义标题已注入: text=%s",
task_id,
user_title_cfg.get("text", "")[:30],
)
except (json.JSONDecodeError, TypeError):
logger.warning("[task_id=%s] custom_title JSON解析失败: %s", task_id, custom_title[:100])
# 用户自定义 BGM 覆盖模板 BGM(用户指定优先级最高)
if bgm_config:
plan_cfg = virtual_plan.config or {}
template_bgm = plan_cfg.get("bgm", {}) or {}
if not isinstance(template_bgm, dict):
template_bgm = {}
merged_bgm = merge_bgm_config(template_bgm, bgm_config)
plan_cfg["bgm"] = merged_bgm
virtual_plan.config = plan_cfg
@@ -1189,6 +1056,8 @@ def _render_video(
plan_cfg = dict(virtual_plan.config or {})
plan_cfg["voice_id"] = voice_ids[0]
subtitle_cfg = plan_cfg.get("subtitle", {}) or {}
if not isinstance(subtitle_cfg, dict):
subtitle_cfg = {}
subtitle_cfg["auto_generated"] = True
plan_cfg["subtitle"] = subtitle_cfg
virtual_plan.config = plan_cfg
@@ -1501,6 +1370,16 @@ def generate_video(self, task_id: str) -> dict:
# 动态分辨率:优先使用 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
# 防护:前端可能误传宽高比(如 parseInt("9:16") = 9),宽度 < 100 时忽略
if _ow < 100 or _oh < 100:
logger.warning(
"[task_id=%s] output_width/output_height 异常 (%dx%d),回退到默认",
task_id,
_ow,
_oh,
)
_ow = OUTPUT_WIDTH
_oh = OUTPUT_HEIGHT
if _ow != OUTPUT_WIDTH or _oh != OUTPUT_HEIGHT:
_resolved_resolution = f"{_ow}x{_oh}"
else:
@@ -1519,6 +1398,7 @@ def generate_video(self, task_id: str) -> dict:
resolution=_resolved_resolution,
bgm_config=task_info.get("bgm_config", {}),
voice_ids=task_info.get("voice_ids", []),
custom_title=task_info.get("custom_title", ""),
)
if gen_task:
@@ -1550,6 +1430,52 @@ def generate_video(self, task_id: str) -> dict:
_update_task_progress(task_id, 95, "上传完成")
# ── 4.5 封面抽帧 ────────────────────────────────────────────────
# 预览视频上传完成后,提取封面帧写入 gen_task.cover_url
# 这样封面路由(generation_cover.py 步骤A)可以通过 generation_task_id 直接找到
try:
from packages.shared.mediakit_client import get_mediakit_client
mk_client = get_mediakit_client()
if mk_client.is_available:
_update_task_progress(task_id, 96, "提取封面帧")
snapshots = mk_client.extract_frames(
video_url=file_url,
strategy="SpecifiedFrames",
max_frames=1,
)
if snapshots and len(snapshots) > 0:
cover_frame_url = snapshots[0].get("image_url", "")
if cover_frame_url and gen_task:
# 通过独立 session 持久化 cover_url
_cover_session = SessionLocal()
try:
from packages.adapters.sqlalchemy_impl.models import (
GenerationTaskModel,
)
_cover_model = (
_cover_session.query(GenerationTaskModel)
.filter(GenerationTaskModel.id == task_id)
.first()
)
if _cover_model:
_cover_model.cover_url = cover_frame_url
_cover_session.commit()
logger.info(
"[task_id=%s] 封面帧提取成功: %s",
task_id,
cover_frame_url[:80],
)
finally:
_cover_session.close()
else:
logger.warning("[task_id=%s] 封面帧提取返回空结果", task_id)
else:
logger.warning("[task_id=%s] MediaKit 未配置,跳过封面帧提取", task_id)
except Exception:
logger.warning("[task_id=%s] 封面帧提取失败(不影响主流程)", task_id, exc_info=True)
# ── 5. 标记完成 ──────────────────────────────────────────────────
_update_task_status(task_id, "mark_completed", result_count=video_count)
+170 -2
View File
@@ -1,3 +1,4 @@
import shutil
import subprocess
import tempfile
from datetime import datetime, timezone
@@ -206,11 +207,21 @@ def ingest_asset(job_id: str) -> dict:
# 视频类型:生成缩略图(文件还在的时候生成)
thumbnail_url = None
if media_type == "video" and extract_success:
frame_path = None
try:
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
from video_processing.oss_helpers import upload_to_oss
from video_processing.thumbnail_generator import extract_first_frame
frame_path = extract_first_frame(str(local_file), width=640)
thumb_storage_key = f"assets/{job.project_id}/thumbnails/{job_id}.jpg"
thumbnail_url = generate_and_upload_thumbnail(str(local_file), thumb_storage_key)
try:
thumbnail_url = upload_to_oss(frame_path, thumb_storage_key)
finally:
if frame_path:
try:
Path(frame_path).unlink(missing_ok=True)
except Exception:
pass
if thumbnail_url:
logger.info(
"素材缩略图生成成功: job_id=%s url=%s",
@@ -223,6 +234,163 @@ def ingest_asset(job_id: str) -> dict:
job_id,
thumb_err,
)
# ── HEVC 自动转码为 1080p H.264 ──────────────────────────────
# 浏览器 WebCodecs 硬件解码 HEVC 输出黑帧,上传时自动转码
# 失败时降级使用原始文件,不阻塞上传流程
if media_type == "video" and local_file and local_file.exists():
codec = (metadata.get("codec") or "").lower()
if codec in ("hevc", "h265", "hvh1"):
logger.info(
"检测到 HEVC 编码 (codec=%s),启动转码: job_id=%s",
codec,
job_id,
)
_tc_tmp = None
_needs_rotation = False
try:
# 检查磁盘空间(大文件转码需要至少 2GB 可用空间)
_disk_usage = shutil.disk_usage("/tmp")
_free_gb = _disk_usage.free / (1024**3)
if _free_gb < 2:
raise RuntimeError(f"磁盘空间不足 ({_free_gb:.1f}GB < 2GB),无法处理大文件")
# 检测视频是否有 rotation 元数据(竖屏视频)
_probe_cmd = [
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"side_data=rotation",
"-show_entries",
"stream_tags=rotate",
"-of",
"default=noprint_wrappers=1:nokey=1",
str(local_file),
]
_probe_result = subprocess.run(
_probe_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
timeout=10,
)
_rotation_str = (_probe_result.stdout or "").strip().split("\n")[0]
if _rotation_str in ("90", "270", "-90"):
_needs_rotation = True
logger.info(
"检测到竖屏视频 (rotation=%s),将物理旋转画面: job_id=%s",
_rotation_str,
job_id,
)
_tc_tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix="_h264.mp4")
_tc_tmp = Path(_tc_tmp_file.name)
_tc_tmp_file.close() # 关闭文件描述符,ffmpeg 会自己打开
# 构建 video filter:竖屏先旋转再缩放
if _needs_rotation:
_vf = "transpose=1,scale='if(gt(ih,1080),-2,iw)':'if(gt(ih,1080),1080,ih)'"
else:
_vf = "scale='if(gt(ih,1080),-2,iw)':'if(gt(ih,1080),1080,ih)'"
_cmd = [
"ffmpeg",
"-y",
"-i",
str(local_file),
"-c:v",
"libx264",
"-preset",
"fast",
"-crf",
"18",
"-vf",
_vf + ",format=yuv420p",
"-colorspace",
"bt709",
"-color_primaries",
"bt709",
"-color_trf",
"bt709",
"-pix_fmt",
"yuv420p",
"-level",
"4.0",
]
# 竖屏视频:清除旋转元数据
if _needs_rotation:
_cmd.extend(["-metadata:s:v:0", "rotate=0"])
_cmd.extend(
[
"-c:a",
"aac",
"-b:a",
"128k",
"-movflags",
"+faststart",
str(_tc_tmp),
]
)
_proc = subprocess.run(
_cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
timeout=900,
)
if _proc.returncode == 0 and _tc_tmp.exists() and _tc_tmp.stat().st_size > 0:
from video_processing.oss_helpers import upload_to_oss
_p = Path(job.storage_key)
_new_key = str(_p.parent / (_p.stem + "_h264" + _p.suffix))
_url = upload_to_oss(_tc_tmp, _new_key)
if _url:
# 先提取元数据,确认成功后再更新 storage_key(避免脏数据)
_new_metadata, _new_extract_success = extract_media_metadata(
str(_tc_tmp),
media_type,
)
if _new_extract_success:
job.storage_key = _new_key
metadata = _new_metadata
extract_success = _new_extract_success
logger.info(
"HEVC→H.264 转码完成: job_id=%s key=%s",
job_id,
_new_key[:80],
)
else:
logger.warning(
"转码文件上传 OSS 失败,使用原始文件: job_id=%s",
job_id,
)
else:
_tail = _proc.stderr[-300:] if _proc.stderr else ""
logger.warning(
"FFmpeg 转码失败 rc=%s,降级原始文件: job_id=%s",
_proc.returncode,
job_id,
)
except subprocess.TimeoutExpired:
logger.warning(
"FFmpeg 转码超时 (900s),降级原始文件: job_id=%s",
job_id,
)
except Exception as _e:
logger.warning(
"HEVC 转码异常(降级原始文件): job_id=%s err=%s",
job_id,
_e,
)
finally:
if _tc_tmp and _tc_tmp.exists():
try:
_tc_tmp.unlink()
except OSError:
pass
finally:
if local_file and local_file.exists():
try:
+43
View File
@@ -0,0 +1,43 @@
# ============================================================
# API 基础镜像(预构建)
# 预装系统依赖 + Python 依赖,业务构建从此镜像开始
# 当 requirements-base.txt 或 requirements.txt 变更时重新构建
# 目标:将 API Image 构建时间从 15-20 分钟降至 3-5 分钟
# ============================================================
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim
# 使用阿里云镜像加速
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 预装系统依赖(gcc 编译 psycopg/pg 扩展,libpq-dev 编译期,libpq5 运行期,ffmpeg 封面取帧)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
libpq5 \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# 创建虚拟环境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /tmp
# 预装 Python 基础依赖
COPY requirements-base.txt requirements.txt ./
RUN pip install --no-cache-dir \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
-r requirements-base.txt -r requirements.txt
# 虚拟环境瘦身
RUN find /opt/venv -name "*.so" -type f -exec strip --strip-all {} \; 2>/dev/null || true
RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
find /opt/venv -name "*.pyc" -delete 2>/dev/null || true
# 清理临时文件
RUN rm -f /tmp/requirements-base.txt /tmp/requirements.txt
ENV PYTHONPATH=/app
+12 -66
View File
@@ -1,81 +1,28 @@
# ============================================================
# API Dockerfile - FastAPI 应用
# 优化:多阶段构建 + pip cache mount + 依赖分层缓存
# 优化:从预构建基础镜像开始,仅叠加业务代码
# 基础镜像包含所有系统依赖和 Python 依赖,构建时间 < 5 分钟
# ============================================================
# ==================== Builder 阶段 ====================
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim AS builder
# 使用阿里云镜像加速
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 安装编译依赖(仅 builder 需要)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# 创建虚拟环境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /tmp
# ---- 依赖分层:基础依赖(变化少,缓存命中率高)----
COPY requirements-base.txt /tmp/requirements-base.txt
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
-r /tmp/requirements-base.txt \
&& rm /tmp/requirements-base.txt
# ---- 依赖分层:业务依赖(变化频繁)----
COPY requirements.txt /tmp/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
-r /tmp/requirements.txt \
&& rm /tmp/requirements.txt
# ---- Python 依赖瘦身 ----
RUN find /opt/venv -name "*.so" -type f -exec strip --strip-all {} \; 2>/dev/null || true
RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
find /opt/venv -name "*.pyc" -delete 2>/dev/null || true
# ==================== Runtime 阶段 ====================
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim AS runtime
FROM xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/saas-api-base:latest
# 构建参数:版本号(CI 传入 commit hash
ARG APP_VERSION=dev
# 使用阿里云镜像加速
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 只装运行时需要的库(libpq5 是 psycopg2 运行时依赖,ffmpeg 用于封面兜底取帧)
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*
# 从 builder 复制虚拟环境
COPY --from=builder /opt/venv /opt/venv
# 设置工作目录
WORKDIR /app
# 复制应用代码
COPY apps/api/ /app/apps/api/
COPY packages/ /app/packages/
COPY alembic.ini /app/alembic.ini
COPY migrations/ /app/migrations/
COPY alembic/ /app/alembic/
COPY scripts/ /app/scripts/
# 复制应用代码(按变化频率从低到高排序,最大化层缓存命中)
COPY alembic.ini ./alembic.ini
COPY migrations/ ./migrations/
COPY alembic/ ./alembic/
COPY scripts/ ./scripts/
COPY packages/ ./packages/
COPY apps/api/ ./apps/api/
# 设置环境变量
ENV PATH="/opt/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
ENV PYTHONPATH=/app
ENV PYTHONPATH=/app:/app/apps/api
ENV PYTHONUNBUFFERED=1
ENV APP_VERSION=$APP_VERSION
@@ -84,5 +31,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5)"
# API 入口点
WORKDIR /app/apps/api
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["uvicorn", "apps.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
+49
View File
@@ -0,0 +1,49 @@
# ============================================================
# Worker 统一基础镜像(预构建)
# 预装系统依赖 + Python 全部依赖 + CJK 字体
# 业务构建从此镜像开始,只需 COPY 业务代码,构建时间 < 5 分钟
# 当 requirements-*.txt 变更时重新构建
# ============================================================
FROM git.xiaoxiajianji.com/xiaoxia/base/python:3.12-slim
# 使用阿里云镜像加速
RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 预装系统依赖(编译工具 + 运行时 + CJK 字体用于 ASS 字幕渲染)
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
python3-dev \
binutils \
ffmpeg \
libglib2.0-0 \
fonts-noto-cjk \
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/*
# 创建虚拟环境
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /tmp
# 预装全部 Python 依赖(基础 + Worker 大包 + 业务依赖)
COPY requirements-base.txt requirements.txt requirements-worker.txt ./
RUN pip install --no-cache-dir \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
-r requirements-base.txt \
-r requirements-worker.txt \
-r requirements.txt
# 虚拟环境瘦身
RUN find /opt/venv -name "*.so" -type f -exec strip --strip-all {} \; 2>/dev/null || true
RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
find /opt/venv -name "*.pyc" -delete 2>/dev/null || true
# 清理临时文件
RUN rm -f /tmp/requirements-base.txt /tmp/requirements.txt /tmp/requirements-worker.txt
ENV PYTHONPATH=/app:/app/packages
+15 -46
View File
@@ -1,51 +1,16 @@
# ============================================================
# Worker Dockerfile - 分层缓存优化版
# 优化:基础依赖 + Worker大包预构建基础镜像,业务构建仅叠加业务依赖
# 基础镜像worker-base-builder / worker-base-runtime
# Worker Dockerfile - 极简化
# 预构建统一基础镜像开始,仅叠加业务代码
# 基础镜像包含:系统依赖 + 全部 Python 依赖 + CJK 字体
# 构建时间目标:< 5 分钟
# ============================================================
# ==================== Builder 阶段 ====================
# 从预构建的builder基础镜像开始,已经包含:
# - 编译工具 (gcc/g++/python3-dev/binutils)
# - requirements-base.txt 全部依赖
# - requirements-worker.txt 全部依赖 (numpy/scipy/opencv)
# - 预strip的.so文件
FROM xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/worker-base-builder:latest AS builder
FROM xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/saas-worker-base:latest
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /tmp
# ---- 安装业务依赖(变化频繁,单独一层)----
COPY requirements.txt /tmp/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \
-r /tmp/requirements.txt \
&& rm /tmp/requirements.txt
# ---- 增量瘦身(清理新增业务依赖的冗余文件)----
RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
find /opt/venv -name "*.pyc" -delete 2>/dev/null || true
# ==================== Runtime 阶段 ====================
# 从预构建的runtime基础镜像开始,已经包含:
# - ffmpeg
# - libglib2.0-0
FROM xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji/worker-base-runtime:latest AS runtime
# 构建参数:版本号
# 构建参数:版本号(CI 传入 commit hash
ARG APP_VERSION=dev
# 从 builder 复制 Python 虚拟环境
COPY --from=builder /opt/venv /opt/venv
# 设置 Python 环境变量
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONPATH=/app:/app/packages
ENV PYTHONUNBUFFERED=1
ENV APP_VERSION=$APP_VERSION
# 创建非 root 用户(极少变化,放最前)
# 创建非 root 用户
RUN groupadd -r celery \
&& useradd -r -g celery -d /app -s /sbin/nologin celery \
&& mkdir -p /app/generated \
@@ -53,14 +18,20 @@ RUN groupadd -r celery \
WORKDIR /app
# 复制文件按变化频率从低到高排序,最大化层缓存命中
# 设置 Python 环境变量
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONPATH=/app:/app/packages
ENV PYTHONUNBUFFERED=1
ENV APP_VERSION=$APP_VERSION
# 复制文件(按变化频率从低到高排序,最大化层缓存命中)
COPY alembic.ini /app/alembic.ini
COPY migrations/ /app/migrations/
COPY packages/ /app/packages/
COPY apps/api/app/config.py /app/apps/api/app/config.py
COPY apps/api/app/core/ /app/apps/api/app/core/
# 复制 Worker 启动脚本
# Worker 启动脚本
COPY infra/docker/entrypoint-worker.sh /usr/local/bin/entrypoint-worker.sh
RUN chmod +x /usr/local/bin/entrypoint-worker.sh
@@ -68,7 +39,5 @@ RUN chmod +x /usr/local/bin/entrypoint-worker.sh
COPY apps/worker/ /app/apps/worker/
USER celery
# Worker 入口点
WORKDIR /app/apps/worker
CMD ["/usr/local/bin/entrypoint-worker.sh"]
@@ -101,6 +101,13 @@ class SQLAlchemyAssetRepository:
return None
return self._to_domain(model)
def find_by_ids(self, asset_ids: list[str]) -> list[Asset]:
"""批量查询素材(单次 SQL IN 查询,避免 N+1)。"""
if not asset_ids:
return []
models = self.session.query(AssetModel).filter(AssetModel.id.in_(asset_ids)).all()
return [self._to_domain(m) for m in models]
def get(self, asset_id: str) -> Asset | None:
return self.find_by_id(asset_id)
+1
View File
@@ -32,6 +32,7 @@ class CreateGenerationTaskCommand:
output_height: int = 720
cover_url: str = ""
custom_title: str = ""
title_config: dict = field(default_factory=dict)
class CreateGenerationTaskUseCase:
+24 -3
View File
@@ -247,6 +247,27 @@ def build_ass_content(
title_config = title_config or {}
subtitle_config = subtitle_config or {}
# ── 兼容前端简化格式:stroke/shadow 为 boolean 时,转换为标准 dict ──
# 前端 TitleSettings 发送 stroke=true/false, shadow=true/false
# 后端 build_ass_style 期望 stroke={enabled, color, width}, shadow={enabled, blur, offset_x, offset_y}
if title_config:
_stroke_val = title_config.get("stroke")
if isinstance(_stroke_val, bool):
title_config["stroke"] = {
"enabled": _stroke_val,
"color": "#000000",
"width": 2,
} if _stroke_val else {"enabled": False}
_shadow_val = title_config.get("shadow")
if isinstance(_shadow_val, bool):
title_config["shadow"] = {
"enabled": _shadow_val,
"color": "#000000",
"blur": 4,
"offset_x": 2,
"offset_y": 2,
} if _shadow_val else {"enabled": False}
title_enabled = title_config.get("enabled", True) and bool(title_text.strip())
subtitle_enabled = subtitle_config.get("enabled", True) and bool(subtitle_text.strip())
@@ -262,7 +283,7 @@ def build_ass_content(
title_stroke = title_config.get("stroke", {}) or {}
title_shadow = title_config.get("shadow", {}) or {}
stroke_color = hex_to_ass_color(title_stroke.get("color", "#000000"))
stroke_width = float(title_stroke.get("width", 1)) if title_stroke.get("enabled", False) else 0.0
stroke_width = float(title_stroke.get("width", 2)) if title_stroke.get("enabled", False) else 0.0
shadow_blur = float(title_shadow.get("blur", 4)) if title_shadow.get("enabled", False) else 0.0
shadow_offset = (
title_shadow.get("offset_x", 2) if title_shadow.get("enabled", False) else 0,
@@ -275,7 +296,7 @@ def build_ass_content(
build_ass_style(
"TitleStyle",
font_name=title_config.get("font", "思源黑体"),
font_size=int(title_config.get("size", 48)),
font_size=min(int(title_config.get("size", 36)), 36),
primary_color=title_color,
outline_color=stroke_color,
outline_width=stroke_width,
@@ -292,7 +313,7 @@ def build_ass_content(
# 根据视频宽度和字号自动换行标题,防止超出画面
# 先 escape 特殊字符,再插入换行符 \N,避免顺序颠倒导致 \N 被转义
title_font_size = int(title_config.get("size", 48))
title_font_size = min(int(title_config.get("size", 36)), 36)
safe_title_text_raw = escape_ass_text(title_text)
safe_title_text = _wrap_title_text(safe_title_text_raw, video_width, title_font_size)
+1 -1
View File
@@ -95,7 +95,7 @@ class ChromaKeyConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> ChromaKeyConfig:
"""从字典解析配置,参数越界自动钳制."""
if not data or not data.get("enabled", False):
if not isinstance(data, dict) or not data.get("enabled", False):
return cls(enabled=False)
key_color = str(data.get("key_color", DEFAULT_KEY_COLOR)).strip()
+1 -1
View File
@@ -196,7 +196,7 @@ class ColorGradeConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "ColorGradeConfig":
"""从字典解析配置."""
if not data or not data.get("enabled", False):
if not isinstance(data, dict) or not data.get("enabled", False):
return cls(enabled=False)
preset = data.get("preset", "")
+6
View File
@@ -121,6 +121,7 @@ class GenerationTask:
output_height: int = 720
cover_url: str = ""
custom_title: str = ""
extra_meta: dict = field(default_factory=dict)
logs: str = "[]"
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@@ -152,6 +153,7 @@ class GenerationTask:
output_height: int = 720,
cover_url: str = "",
custom_title: str = "",
extra_meta: dict | None = None,
) -> "GenerationTask":
if not project_id.strip() and not template_id.strip():
raise ValueError("project_id 或 template_id 至少需要提供一个")
@@ -182,6 +184,7 @@ class GenerationTask:
output_height=output_height,
cover_url=cover_url,
custom_title=custom_title,
extra_meta=dict(extra_meta) if extra_meta else {},
)
# ── 状态查询 ────────────────────────────────────────────────────────────
@@ -301,6 +304,7 @@ class GenerationTask:
*,
cover_url: str = "",
custom_title: str = "",
extra_meta: dict | None = None,
output_width: int = 0,
output_height: int = 0,
) -> None:
@@ -318,6 +322,8 @@ class GenerationTask:
self.output_width = output_width
if output_height > 0:
self.output_height = output_height
if extra_meta:
self.extra_meta.update(extra_meta)
self.updated_at = datetime.now(timezone.utc)
# ── 日志辅助 ────────────────────────────────────────────────────────────
+1 -1
View File
@@ -76,7 +76,7 @@ class IntroOutroConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "IntroOutroConfig":
"""从字典构造."""
if not data:
if not isinstance(data, dict):
return cls()
enabled = data.get("enabled", False)
+1 -1
View File
@@ -81,7 +81,7 @@ class NoiseReductionConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> NoiseReductionConfig:
"""从字典解析配置,参数越界自动钳制."""
if not data or not data.get("enabled", False):
if not isinstance(data, dict) or not data.get("enabled", False):
return cls(enabled=False)
level_str = str(data.get("level", "medium")).lower()
+1 -1
View File
@@ -136,7 +136,7 @@ class PiPConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "PiPConfig":
"""从字典解析配置."""
if not data or not data.get("enabled", False):
if not isinstance(data, dict) or not data.get("enabled", False):
return cls(enabled=False)
layers_data = data.get("layers", [])
+1 -1
View File
@@ -86,7 +86,7 @@ class WatermarkConfig:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> WatermarkConfig | None:
"""从字典构造,空配置返回 None(不加水印)."""
if not data:
if not isinstance(data, dict):
return None
enabled = data.get("enabled", False)
+11 -174
View File
@@ -13,8 +13,6 @@ import random
import time
from typing import Any, Dict, List, Optional
import requests as http_requests
from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG
from packages.shared.ai_client import get_doubao_client
@@ -354,93 +352,6 @@ def _transfer_cover_frame_to_storage(frame_url: str, plan_id: str) -> str:
return frame_url
def _extract_frames_with_ffmpeg(
video_url: str,
num_frames: int = 3,
timeout: int = 30,
) -> list[dict]:
"""用 FFmpeg 从远程视频 URL 流式 seek 抽帧(HTTP range request,不下载整个视频)。
Args:
video_url: 视频 URL
num_frames: 抽帧数量
timeout: 单帧超时
Returns:
[{"local_path": "...", "frame_time": 5.0}, ...]
"""
import re as _re
import tempfile
from pathlib import Path as _Path
from packages.shared.ffmpeg_utils import FFMPEG_BIN, run_ffmpeg
video_url = _re.sub(r"(?<!:)//", "/", video_url)
# 先用 ffprobe 获取视频时长
import subprocess as _subprocess
from packages.shared.ffmpeg_utils import FFPROBE_BIN
duration = 30.0 # 默认假设 30 秒
try:
probe_result = _subprocess.run(
[
FFPROBE_BIN,
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
video_url,
],
capture_output=True,
text=True,
timeout=15,
)
if probe_result.returncode == 0 and probe_result.stdout.strip():
duration = float(probe_result.stdout.strip())
except Exception as e:
logger.warning("FFprobe 远程视频时长失败,使用默认值: %s", e)
ratios = [i / (num_frames + 1) for i in range(1, num_frames + 1)]
results = []
for _idx, ratio in enumerate(ratios):
frame_time = max(0.5, duration * ratio)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
output_path = tmp.name
try:
seek_str = f"{int(frame_time // 3600):02d}:{int((frame_time % 3600) // 60):02d}:{frame_time % 60:05.2f}"
cmd = [
FFMPEG_BIN,
"-y",
"-ss",
seek_str,
"-i",
video_url,
"-vframes",
"1",
"-q:v",
"2",
output_path,
]
run_ffmpeg(cmd, capture_output=True, timeout=timeout)
if _Path(output_path).exists() and _Path(output_path).stat().st_size > 0:
results.append({"local_path": output_path, "frame_time": round(frame_time, 2)})
else:
_Path(output_path).unlink(missing_ok=True)
except Exception as e:
logger.warning("FFmpeg 远程抽帧失败 ratio=%.2f: %s", ratio, e)
_Path(output_path).unlink(missing_ok=True)
return results
def _call_ai_cover_service(
plan_id: str,
asset_ids: List[str],
@@ -450,9 +361,9 @@ def _call_ai_cover_service(
) -> Dict[str, Any]:
"""调用 AI 封面生成服务.
优先级
1. 检查 plan.config 中的 cover_candidates渲染时预抽帧由调用方处理
2. FFmpeg 本地从 URL 流式 seek 抽帧HTTP range request不下载整个视频
统一封面管道下封面已由渲染后视频抽帧生成并持久化到 GenerationTask.cover_url
此函数仅处理 manual/upload 等需要前端交互的类型
ai_frame/ai_regenerate 类型应由调用方直接从持久化的封面 URL 读取
失败时抛出 RuntimeError
@@ -484,88 +395,14 @@ def _call_ai_cover_service(
"frame_time": frame_time,
}
# ai_frame / ai_regenerate - 使用 FFmpeg 本地抽帧
if primary_video_url:
import re as _re
primary_video_url = _re.sub(r"(?<!:)//", "/", primary_video_url)
# 先检查视频 URL 是否可访问
try:
head_resp = http_requests.head(primary_video_url, timeout=10, allow_redirects=True)
if head_resp.status_code != 200:
logger.error(
"封面视频URL不可访问: plan_id=%s url=%s status=%d",
plan_id,
primary_video_url,
head_resp.status_code,
)
raise RuntimeError(
f"封面生成失败: 预览视频URL不可访问 (HTTP {head_resp.status_code})。" f"请重新生成预览视频后再试。"
)
except http_requests.RequestException as e:
logger.error("封面视频URL连通性检查失败: plan_id=%s url=%s error=%s", plan_id, primary_video_url, e)
raise RuntimeError(
f"封面生成失败: 无法访问预览视频 ({e.__class__.__name__})。请重新生成预览视频后再试。"
) from e
# 使用 FFmpeg 从 URL 流式 seek 抽帧
try:
logger.info("FFmpeg 远程抽帧: plan_id=%s video=%s", plan_id, primary_video_url[:80])
frames = _extract_frames_with_ffmpeg(primary_video_url, num_frames=3)
if frames:
best_frame = frames[0]
local_path = best_frame["local_path"]
frame_time_val = best_frame["frame_time"]
# 上传到 OSS
try:
import uuid
from pathlib import Path
from packages.shared.storage import get_shared_storage_service
storage = get_shared_storage_service()
cover_key = f"covers/{plan_id}/ffmpeg_frame_{uuid.uuid4().hex[:8]}.jpg"
storage.upload_file(
file_or_path=local_path,
storage_key=cover_key,
content_type="image/jpeg",
)
public_url = storage.get_url(cover_key)
logger.info(
"FFmpeg 抽帧成功: plan_id=%s frame_time=%.2f url=%s",
plan_id,
frame_time_val,
public_url[:80],
)
return {
"type": "ai_frame",
"image_url": public_url,
"frame_time": round(frame_time_val, 1),
"confidence": 0.85,
}
finally:
# 清理所有临时文件
for frame in frames:
try:
Path(frame["local_path"]).unlink(missing_ok=True)
except Exception:
pass
except RuntimeError:
raise
except Exception as e:
logger.exception("FFmpeg 远程抽帧失败: %s", str(e))
# 封面生成失败
raise RuntimeError(f"封面生成失败: plan_id={plan_id},无法从视频抽帧。请检查 primary_video_url 是否可访问。")
# ── 公共入口 ────────────────────────────────────────────────────────────────
# ai_frame / ai_regenerate: 封面应由渲染后视频抽帧管道生成
# 如果调用方传入了持久化的封面 URL,直接使用
logger.warning(
"封面生成回退: plan_id=%s cover_type=%s — 统一管道应已生成封面,请检查 GenerationTask.cover_url",
plan_id,
cover_type,
)
raise RuntimeError(f"封面数据不可用 (plan_id={plan_id})。请重新生成预览视频以触发封面自动提取。")
def run_ai_recommend(
-1
View File
@@ -2,7 +2,6 @@
# 修改此文件会触发完整重新构建,请谨慎修改
# 数据库(基础层)
psycopg2-binary==2.9.9
psycopg[binary]==3.2.2
sqlalchemy==2.0.35
alembic==1.13.3
+13 -3
View File
@@ -3,6 +3,9 @@
# 用法: docker_build_push.sh [--no-cache] <Dockerfile> <image_tag> <cache_ref> [build_arg...]
set -eu
# 单次 build 超时时间(秒),防止 docker buildx build 无限挂起
BUILD_TIMEOUT=1500
NO_CACHE_FLAG=""
if [ "$1" = "--no-cache" ]; then
NO_CACHE_FLAG="--no-cache"
@@ -43,7 +46,7 @@ build_with_cache_retry() {
local build_output
local exit_code
set +e
build_output=$(docker buildx build \
build_output=$(timeout ${BUILD_TIMEOUT} docker buildx build \
$NO_CACHE_FLAG \
$BUILD_ARGS \
--cache-from "type=local,src=${LOCAL_CACHE_DIR}" \
@@ -60,6 +63,12 @@ build_with_cache_retry() {
echo "$build_output"
return 0
fi
# 超时退出(exit code 124
if [ $exit_code -eq 124 ]; then
echo "❌ Docker build TIMEOUT after ${BUILD_TIMEOUT}s - build hung and was killed"
echo "$build_output" | tail -20
return $exit_code
fi
# 检测到缓存损坏类错误,清掉本地缓存重试
if echo "$build_output" | grep -qE "parent snapshot.*not found|snapshot.*does not exist|cache.*corrupt|failed to compute cache key"; then
echo "$build_output"
@@ -68,7 +77,7 @@ build_with_cache_retry() {
rm -rf "${LOCAL_CACHE_DIR}"
mkdir -p "${LOCAL_CACHE_DIR}"
# 清理buildx builder的内部snapshot状态
docker buildx prune -f -a >/dev/null 2>&1 || true
docker buildx prune -f -a > /dev/null 2>&1 || true
attempt=$((attempt + 1))
else
# 非缓存类错误,直接输出并返回
@@ -78,7 +87,7 @@ build_with_cache_retry() {
done
# 重试完还是失败,不用本地缓存最后试一次(只从registry读)
echo "⚠️ All cached attempts failed, building without local cache..."
docker buildx build \
timeout ${BUILD_TIMEOUT} docker buildx build \
$NO_CACHE_FLAG \
$BUILD_ARGS \
--cache-from "type=registry,ref=${CACHE_REF}" \
@@ -93,6 +102,7 @@ build_with_cache_retry() {
echo "=== Step 1: Build & push image (local cache + registry cache, with auto-repair) ==="
echo "Local cache: ${LOCAL_CACHE_DIR}"
echo "Registry cache: ${CACHE_REF}"
echo "Build timeout: ${BUILD_TIMEOUT}s"
echo ""
build_with_cache_retry
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
# ============================================================
# 重建 API 基础镜像脚本
# 用途:当 requirements-base.txt 或 requirements.txt 变更时手动触发
# 前提:需要在已登录 ACR 的构建服务器上执行
# ============================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
REGISTRY="xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji"
IMAGE_NAME="saas-api-base"
TAG="latest"
FULL_TAG="${REGISTRY}/${IMAGE_NAME}:${TAG}"
echo "========================================="
echo "🔨 Rebuilding API base image"
echo " Registry: ${REGISTRY}"
echo " Image: ${FULL_TAG}"
echo " Context: ${REPO_ROOT}"
echo "========================================="
cd "$REPO_ROOT"
# 构建并推送
docker buildx build \
--platform linux/amd64 \
--tag "${FULL_TAG}" \
--push \
-f infra/docker/api-base.Dockerfile \
.
echo ""
echo "✅ API base image pushed: ${FULL_TAG}"
# 显示镜像大小
docker pull "${FULL_TAG}" > /dev/null 2>&1
docker images "${FULL_TAG}" --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
+69 -173
View File
@@ -260,6 +260,30 @@ def _make_user(**overrides) -> User:
return User(**defaults)
def _direct_insert_asset(client, name="test-video.mp4", storage_key=None, mime_type="video/mp4", status=None):
"""Helper: insert asset directly into repo (bypass deprecated create_asset API)."""
import uuid as _uuid
app = client.app
asset_repo = app.dependency_overrides[get_asset_repository]()
kw = {}
if status is not None:
kw["status"] = status
else:
kw["status"] = AssetStatus.READY
asset = Asset(
id=_uuid.uuid4().hex,
project_id="proj-1",
library_id="lib-1",
name=name,
storage_key=storage_key or f"uploads/{name}",
mime_type=mime_type,
**kw,
)
asset_repo.create(asset)
return asset.id
def _make_project(id: str = "proj-1", owner_user_id: str = "user-test-001") -> Project:
return Project(id=id, name="Test Project", owner_user_id=owner_user_id)
@@ -344,8 +368,8 @@ def client(mock_storage):
class TestCreateAsset:
"""创建素材端点测试。"""
def test_create_asset_success(self, client):
"""正常创建素材成功"""
def test_create_asset_returns_410_gone(self, client):
"""create_asset 已废弃,返回 410 Gone 提示使用 ingest-jobs"""
resp = client.post(
"/api/v1/assets",
json={
@@ -354,59 +378,23 @@ class TestCreateAsset:
"name": "new-video.mp4",
"storage_key": "uploads/new-video.mp4",
"mime_type": "video/mp4",
"file_size": 2048,
"duration": 15.0,
},
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "new-video.mp4"
assert data["project_id"] == "proj-1"
assert data["library_id"] == "lib-1"
assert data["mime_type"] == "video/mp4"
assert "id" in data
assert data["status"] == "uploading"
assert resp.status_code == 410
def test_create_asset_project_not_found(self, client):
"""项目不存在返回 404"""
def test_create_asset_any_type_returns_410(self, client):
"""所有类型都返回 410 Gone(图片/音频也废弃)"""
resp = client.post(
"/api/v1/assets",
json={
"project_id": "nonexistent",
"project_id": "proj-1",
"library_id": "lib-1",
"name": "test.mp4",
"storage_key": "uploads/test.mp4",
"mime_type": "video/mp4",
"name": "photo.jpg",
"storage_key": "uploads/photo.jpg",
"mime_type": "image/jpeg",
},
)
assert resp.status_code == 404
assert "Project" in resp.json()["detail"]
def test_create_asset_library_not_found(self, client):
"""素材库不存在返回 404。"""
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "nonexistent",
"name": "test.mp4",
"storage_key": "uploads/test.mp4",
"mime_type": "video/mp4",
},
)
assert resp.status_code == 404
assert "AssetLibrary" in resp.json()["detail"]
def test_create_asset_missing_required_fields(self, client):
"""缺少必填字段返回 422。"""
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"name": "test.mp4",
},
)
assert resp.status_code == 422
assert resp.status_code == 410
# ---------------------------------------------------------------------------
@@ -418,20 +406,26 @@ class TestListAssets:
"""获取素材列表端点测试。"""
def _create_test_assets(self, client, count: int = 3):
"""辅助方法:创建测试素材(status=ready)。"""
"""辅助方法:直接插入测试素材到 repository(绕过已废弃的 create_asset API)。"""
# 通过依赖覆盖获取 asset_repo
app = client.app
asset_repo = app.dependency_overrides.get(get_asset_repository, lambda: None)()
if asset_repo is None:
return
for i in range(count):
client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": f"video-{i}.mp4",
"storage_key": f"uploads/video-{i}.mp4",
"mime_type": "video/mp4",
"file_size": 1024 * (i + 1),
"status": "ready",
},
import uuid
asset = Asset(
id=uuid.uuid4().hex,
project_id="proj-1",
library_id="lib-1",
name=f"video-{i}.mp4",
storage_key=f"uploads/video-{i}.mp4",
mime_type="video/mp4",
file_size=1024 * (i + 1),
status=AssetStatus.READY,
)
asset_repo.create(asset)
def test_empty_list(self, client):
"""无素材时返回空列表。"""
@@ -517,17 +511,7 @@ class TestListAssets:
def test_list_status_filter_uploading_visible(self, client):
"""uploading状态的素材默认能看到(上传后立即显示处理中)。"""
client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "uploading-test.mp4",
"storage_key": "uploads/uploading-test.mp4",
"mime_type": "video/mp4",
"status": "uploading",
},
)
_direct_insert_asset(client, name="uploading-test.mp4", status=AssetStatus.UPLOADING)
resp = client.get("/api/v1/assets?library_id=lib-1")
assert resp.status_code == 200
@@ -537,28 +521,8 @@ class TestListAssets:
def test_list_with_keyword_filter(self, client):
"""按名称关键词过滤。"""
client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "hello-world.mp4",
"storage_key": "uploads/hello.mp4",
"mime_type": "video/mp4",
"status": "ready",
},
)
client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "goodbye.mp4",
"storage_key": "uploads/goodbye.mp4",
"mime_type": "video/mp4",
"status": "ready",
},
)
_direct_insert_asset(client, name="hello-world.mp4")
_direct_insert_asset(client, name="goodbye.mp4", mime_type="video/mp4", status=AssetStatus.READY)
resp = client.get("/api/v1/assets?library_id=lib-1&keyword=hello")
assert resp.status_code == 200
@@ -576,22 +540,8 @@ class TestGetAsset:
"""获取单个素材详情端点测试。"""
def _create_asset(self, client) -> str:
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "detail-test.mp4",
"storage_key": "uploads/detail-test.mp4",
"mime_type": "video/mp4",
"file_size": 5000,
"duration": 25.0,
"width": 1280,
"height": 720,
"fps": 30.0,
},
)
return resp.json()["id"]
"""Direct insert into repo (create_asset API is deprecated/410)."""
return _direct_insert_asset(client)
def test_get_asset_success(self, client):
"""获取存在的素材详情成功。"""
@@ -601,11 +551,7 @@ class TestGetAsset:
assert resp.status_code == 200
data = resp.json()
assert data["id"] == asset_id
assert data["name"] == "detail-test.mp4"
assert data["file_size"] == 5000
assert data["duration"] == 25.0
assert data["width"] == 1280
assert data["height"] == 720
assert data["name"] == "test-video.mp4"
assert "file_url" in data
assert "status" in data
@@ -625,17 +571,8 @@ class TestUpdateAsset:
"""更新素材端点测试。"""
def _create_asset(self, client) -> str:
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "old-name.mp4",
"storage_key": "uploads/old-name.mp4",
"mime_type": "video/mp4",
},
)
return resp.json()["id"]
"""Direct insert into repo (create_asset API is deprecated/410)."""
return _direct_insert_asset(client)
def test_update_asset_name(self, client):
"""更新素材名称成功。"""
@@ -675,7 +612,7 @@ class TestUpdateAsset:
resp = client.put(f"/api/v1/assets/{asset_id}", json={})
assert resp.status_code == 200
assert resp.json()["name"] == "old-name.mp4"
assert resp.json()["name"] == "test-video.mp4"
# ---------------------------------------------------------------------------
@@ -687,17 +624,8 @@ class TestDeleteAsset:
"""删除素材端点测试。"""
def _create_asset(self, client) -> str:
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "delete-test.mp4",
"storage_key": "uploads/delete-test.mp4",
"mime_type": "video/mp4",
},
)
return resp.json()["id"]
"""Direct insert into repo (create_asset API is deprecated/410)."""
return _direct_insert_asset(client)
def test_delete_asset_success(self, client):
"""删除存在的素材成功,返回 204。"""
@@ -737,17 +665,8 @@ class TestBatchDeleteAssets:
def _create_assets(self, client, count: int = 3) -> list[str]:
ids = []
for i in range(count):
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": f"batch-{i}.mp4",
"storage_key": f"uploads/batch-{i}.mp4",
"mime_type": "video/mp4",
},
)
ids.append(resp.json()["id"])
aid = _direct_insert_asset(client, name=f"batch-{i}.mp4")
ids.append(aid)
return ids
def test_batch_delete_success(self, client):
@@ -802,17 +721,8 @@ class TestAssetTags:
"""素材标签相关端点测试。"""
def _create_asset(self, client) -> str:
resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "tag-test.mp4",
"storage_key": "uploads/tag-test.mp4",
"mime_type": "video/mp4",
},
)
return resp.json()["id"]
"""Direct insert into repo (create_asset API is deprecated/410)."""
return _direct_insert_asset(client)
def test_add_tags_to_asset(self, client):
"""给素材打标签。需要先在 tag_repo 中创建标签。"""
@@ -847,22 +757,8 @@ class TestAssetsCRUDFlow:
def test_full_crud_flow(self, client):
"""测试完整的创建 → 列表 → 详情 → 更新 → 删除流程。"""
# 1. 创建
create_resp = client.post(
"/api/v1/assets",
json={
"project_id": "proj-1",
"library_id": "lib-1",
"name": "crud-flow.mp4",
"storage_key": "uploads/crud-flow.mp4",
"mime_type": "video/mp4",
"file_size": 8192,
"metadata": {"source": "test"},
"status": "ready",
},
)
assert create_resp.status_code == 200
asset_id = create_resp.json()["id"]
# 1. 创建 (direct insert since create_asset is 410)
asset_id = _direct_insert_asset(client, name="crud-flow.mp4")
# 2. 列表中应包含
list_resp = client.get("/api/v1/assets?library_id=lib-1")
+17 -62
View File
@@ -14,11 +14,7 @@ import pytest
from video_processing.unified_render_service import (
UnifiedRenderService,
)
from worker_app.tasks.generation import (
_build_plan_and_clips_from_task,
_create_fallback_clip,
_mux_audio_track,
)
from worker_app.tasks.generation import _build_plan_and_clips_from_task
pytestmark = pytest.mark.skipif(
not shutil.which("ffmpeg"),
@@ -62,61 +58,6 @@ def _generate_test_audio(path: Path, duration: float = 5.0) -> None:
subprocess.run(cmd, check=True, capture_output=True, timeout=30)
# ── 测试 _create_fallback_clip ────────────────────────────────────────────────
class TestFallbackClip:
"""测试 fallback 视频生成。"""
def test_fallback_clip_creates_video(self):
with tempfile.TemporaryDirectory() as tmpdir:
output = Path(tmpdir) / "fallback.mp4"
_create_fallback_clip(output, "Test Fallback")
assert output.exists()
assert output.stat().st_size > 0
# ── 测试 _mux_audio_track ────────────────────────────────────────────────────
class TestMuxAudioTrack:
"""测试视频+音频混合。"""
def test_mux_audio_into_video(self):
with tempfile.TemporaryDirectory() as tmpdir:
video_path = Path(tmpdir) / "video.mp4"
audio_path = Path(tmpdir) / "audio.aac"
output_path = Path(tmpdir) / "output.mp4"
_generate_test_video(video_path, duration=3.0)
_generate_test_audio(audio_path, duration=5.0)
_mux_audio_track(video_path, str(audio_path), output_path)
assert output_path.exists()
assert output_path.stat().st_size > 0
# 验证输出文件包含音频轨
probe_cmd = [
"ffprobe",
"-v",
"quiet",
"-show_streams",
"-select_streams",
"a",
"-of",
"csv=p=0",
str(output_path),
]
result = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=10)
# 如果有音频流,输出非空
assert result.stdout.strip() != "" or result.returncode == 0
# ── 测试 PlanGenerator → UnifiedRenderService 全链路 ─────────────────────────
class TestFullPipeline:
"""验证从虚拟 plan 构建到渲染输出的完整流程。"""
@@ -175,12 +116,26 @@ class TestFullPipeline:
)
render_result = service.render()
# 混音
# 混音 - 直接用 ffmpeg_mux_audio_track 已被清理)
audio_path = work_dir / "voice.aac"
_generate_test_audio(audio_path, duration=5.0)
final_path = work_dir / "final.mp4"
_mux_audio_track(render_result.output_path, str(audio_path), final_path)
mux_cmd = [
"ffmpeg",
"-y",
"-i",
str(render_result.output_path),
"-i",
str(audio_path),
"-c:v",
"copy",
"-c:a",
"aac",
"-shortest",
str(final_path),
]
subprocess.run(mux_cmd, check=True, capture_output=True, timeout=30)
assert final_path.exists()
assert final_path.stat().st_size > 0

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