Compare commits

...

91 Commits

Author SHA1 Message Date
xiaoxia 2494447d94 ci: retry build (transient BuildKit infrastructure error)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m32s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m1s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m14s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 59s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m33s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m22s
AI Code Review / AI Code Review (pull_request) Successful in 2m33s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m47s
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 / PR Build API Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m16s
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 / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m4s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m10s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 58s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m20s
CI/CD Pipeline / Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m30s
CI/CD Pipeline / Unit Tests (push) Successful in 11m11s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m59s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
2026-08-25 18:08:10 +08:00
xiaoxia 9cbbf9a6e9 fix: 预览标题换行同时匹配半角/和全角/ (#1500)
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m36s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m14s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m27s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m37s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m25s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m30s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 26s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m22s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m49s
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 / Unit Tests (pull_request) Failing after 16m41s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 18m28s
CI/CD Pipeline / Unit Tests (push) Failing after 19m16s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 20m8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 20m8s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 20m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) 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 / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m59s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m0s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
PreviewVideoPanel 的 displayTitle.split("/") 改为正则 split(/[//]/),
与 useCanvasPlayer.ts 保持一致,中文输入法下的全角斜杠也能触发换行。
2026-08-25 16:45:14 +08:00
xiaoxia 41c1845aa1 fix: /tts/preview 解析克隆音色 UUID 为 CosyVoice voice_id (#1497)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m5s
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) (pull_request) Successful in 3m13s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m14s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m17s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m20s
AI Code Review / AI Code Review (pull_request) Failing after 3m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m6s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m35s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m57s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m21s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m3s
CI/CD Pipeline / Unit Tests (push) Successful in 13m43s
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 (pull_request) Successful in 5m16s
CI/CD Pipeline / Integration Tests (push) Successful in 5m20s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m24s
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 12s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 16:26:16 +08:00
xiaoxia d11ca875c6 fix: 上传的配音素材直接使用 file_url,不调 TTS 合成 (#1499)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 33s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m33s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m32s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m55s
CI/CD Pipeline / Frontend Lint (push) 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 / Build Staging API Image (push) Successful in 4m8s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
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
AI Code Review / AI Code Review (pull_request) Failing after 4m44s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) 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 / 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
2026-08-25 16:17:51 +08:00
xiaoxia a1ddcb6ff6 feat: add POST /tts/preview endpoint for synchronous TTS preview (#1496)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m10s
CI/CD Pipeline / Frontend Lint (push) 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 / 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) (pull_request) Successful in 2m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m52s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m1s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 36s
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
AI Code Review / AI Code Review (pull_request) Failing after 4m2s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m58s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m17s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m35s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 11m7s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m17s
CI/CD Pipeline / Unit Tests (push) Successful in 14m35s
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 5m11s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m32s
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 5m10s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 15:21:01 +08:00
xiaoxia b7d96f1b2e fix: 预览面板缩小 + 配音缺失修复 (#1495)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m10s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 45s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m52s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m6s
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 3m15s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m20s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m56s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m49s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
AI Code Review / AI Code Review (pull_request) Failing after 5m59s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m24s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m59s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 10m45s
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 / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m32s
CI/CD Pipeline / Unit Tests (push) Successful in 13m11s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m6s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 14:02:41 +08:00
xiaoxia f40b081318 fix: 预览面板缩小 + 配音缺失修复 (#1495)
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m20s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m32s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m47s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m39s
AI Code Review / AI Code Review (pull_request) Failing after 3m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m4s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-25 13:58:28 +08:00
xiaoxia d79cf3726b feat: 预览播放器手机风格视觉改造 — 9:16竖屏外框+毛玻璃控制条 (#1494)
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 1m9s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m21s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m33s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m35s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m35s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
AI Code Review / AI Code Review (pull_request) Successful in 3m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m52s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m41s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m18s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m39s
CI/CD Pipeline / Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m19s
CI/CD Pipeline / Unit Tests (push) Successful in 13m41s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m50s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 13:01:30 +08:00
xiaoxia 5a1678c6ee feat: 预览播放器手机风格视觉改造 — 9:16竖屏外框+毛玻璃控制条+精致播放按钮 (#1494)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m34s
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m1s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - 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 / 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-25 12:58:40 +08:00
xiaoxia 35f8af50c1 fix: 预览面板容器高度塌陷 — CSS加高度约束 + 预览区加minHeight/背景 (#1493)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m14s
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 / Build Staging Worker Image (push) Successful in 1m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m16s
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 2m21s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m49s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m51s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m16s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m17s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m33s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m35s
AI Code Review / AI Code Review (pull_request) Successful in 8m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 44s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m17s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m36s
CI/CD Pipeline / Integration Tests (push) Successful in 3m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m24s
CI/CD Pipeline / Unit Tests (push) Successful in 13m30s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m54s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 12:06:00 +08:00
xiaoxia edca702a05 fix: 预览面板容器高度塌陷 — CSS加高度约束 + 预览区加minHeight/背景 (#1493)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) 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
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-25 12:05:36 +08:00
xiaoxia 3e876fc054 fix: FrontendPreviewPlayer 用 relative 容器包裹 Fragment,修复绝对定位失控 (#1492)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) 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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m7s
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 2m49s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m54s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m1s
AI Code Review / AI Code Review (pull_request) Failing after 3m19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 40s
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 / 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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m0s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m4s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 48s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m40s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m46s
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 / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m39s
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 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 11:41:54 +08:00
xiaoxia a69868c532 fix: FrontendPreviewPlayer 用 relative 容器包裹 Fragment,修复绝对定位失控 (#1492)
CI/CD Pipeline / Check if frontend-only change (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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 59s
CI/CD Pipeline / Build Staging Worker 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 - Migration (alembic) (push) Successful in 2m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m11s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m25s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m24s
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 / 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-25 11:38:01 +08:00
xiaoxia 5f87bede1a fix: FrontendPreviewPlayer video fallback 路径补充标题叠加层 (#1491)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m15s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m17s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m49s
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 / PR Build API Image (pull_request) Successful in 37s
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 / 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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m53s
AI Code Review / AI Code Review (pull_request) Successful in 5m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 53s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m12s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m27s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Integration Tests (push) Successful in 4m7s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m34s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m48s
CI/CD Pipeline / Unit Tests (push) Successful in 14m2s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m42s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 11:03:28 +08:00
xiaoxia dab88e0e66 fix: FrontendPreviewPlayer video fallback 路径补充标题叠加层 (#1491)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m24s
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 / 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 2m2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m11s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m12s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m13s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - 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 / 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-25 10:59:34 +08:00
xiaoxia 16bc4f53bf feat: 预览改为前端实时播放,确认生成时一次服务器渲染 (#1490)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m11s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m15s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m50s
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 Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m54s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m24s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m57s
AI Code Review / AI Code Review (pull_request) Failing after 6m8s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m30s
CI/CD Pipeline / Integration Tests (push) Successful in 4m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m29s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 12m56s
CI/CD Pipeline / Unit Tests (push) Successful in 13m13s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m1s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 7m15s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 10:01:01 +08:00
xiaoxia b05f34432b fix: 预览渲染根据video_ratio计算分辨率,不再默认1280x720 (#1489)
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 49s
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 / Check if frontend-only change (pull_request) Successful in 59s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Failing after 1m47s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m53s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m59s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 5m6s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m24s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m11s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m56s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m37s
CI/CD Pipeline / Integration Tests (push) Successful in 4m4s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m34s
CI/CD Pipeline / Unit Tests (push) Successful in 14m54s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m40s
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 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 09:36:01 +08:00
xiaoxia 5b2d5901f0 fix: 预览请求补传 output_width/output_height,匹配 video_ratio (#1488)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 53s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 59s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m46s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m41s
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 3m55s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m56s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m12s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m5s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 5m46s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m11s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m35s
AI Code Review / AI Code Review (pull_request) Successful in 9m1s
CI/CD Pipeline / Integration Tests (push) Successful in 4m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m20s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m30s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m30s
CI/CD Pipeline / Unit Tests (push) Successful in 13m39s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m26s
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 13s
2026-08-25 09:10:51 +08:00
xiaoxia ebb664668b fix: 预览请求补传 output_width/output_height,匹配 video_ratio
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m5s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m30s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 1m57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m5s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m9s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m37s
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 15s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m3s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m31s
- 新增 calculateResolution 共享工具函数,预览和生成共用同一份分辨率计算
- CreatePreviewRequest 类型新增 output_width/output_height 字段
- buildPreviewRequest 传入计算后的分辨率(9:16 → 1080×1920)
- useGenerateVideo 改用共享函数,去除重复的内联计算
2026-08-25 09:03:34 +08:00
xiaoxia 055f6c0475 feat: 预览改为服务器渲染视频,实现所见即所得 (#1487)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 53s
CI/CD Pipeline / Frontend Lint (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 / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m11s
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 3m7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m36s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m47s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m16s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 49s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m33s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
AI Code Review / AI Code Review (pull_request) Successful in 7m46s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 11m8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m16s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
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 / 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 / Unit Tests (pull_request) Successful in 13m40s
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 (push) Successful in 6m32s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
2026-08-25 01:00:55 +08:00
xiaoxia d1890ab128 feat: 预览改为服务器渲染视频,实现所见即所得
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 51s
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 / Frontend Unit Tests (pull_request) Successful in 47s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m44s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m23s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m21s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m50s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 4m11s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m26s
AI Code Review / AI Code Review (pull_request) Successful in 7m2s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m11s
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 8s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m6s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m10s
- 新增 useServerPreview hook:自动创建/轮询服务器预览任务
- PreviewVideoPanel 用 <video> 播放服务器渲染结果,替换 Canvas 播放器
- 素材/配音/BGM 变更自动重新渲染;标题变更标记 stale
- Step5 需预览完成才能前进,确认生成走 confirm 路径秒出
- 轮询间隔 2s,超时 120s,网络错误自动重试 2 次
- 标题样式编辑时 CSS 层实时叠加预览
2026-08-25 00:49:40 +08:00
xiaoxia 7844b65afe feat: create端点兜底复用预览产物 + confirm同步标题 (#1486)
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 / Check if frontend-only change (pull_request) Successful in 1m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m31s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m40s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m49s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m35s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m52s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m12s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m9s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m45s
AI Code Review / AI Code Review (pull_request) Successful in 8m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m48s
CI/CD Pipeline / Unit Tests (push) Successful in 14m50s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m29s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 23:31:00 +08:00
xiaoxia ccb5ed816a fix: previewTaskId 持久化 + 配音/BGM 参数完整传递 (#1485)
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 1m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m29s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m50s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m53s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m21s
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) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
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
AI Code Review / AI Code Review (pull_request) Failing after 3m24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m52s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m28s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m4s
CI/CD Pipeline / Integration Tests (push) Successful in 4m35s
CI/CD Pipeline / Unit Tests (push) Successful in 14m13s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m37s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 22:56:31 +08:00
xiaoxia 3af65c2743 fix: 渲染产物临时目录不在render_plan中提前清理,改由调用方上传后清理 (#1484)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m22s
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 / 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) (pull_request) Successful in 3m2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m28s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m55s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 2m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m0s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m27s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m26s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 12m19s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m24s
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 / 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 / Unit Tests (pull_request) Successful in 13m38s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m53s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m58s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 21:21:00 +08:00
xiaoxia fb4145f6de fix: 兜底关联edit plan和回写config移到enqueue之前,消除竞态条件 (#1482)
CI/CD Pipeline / Check if frontend-only change (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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m57s
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) (pull_request) Successful in 2m49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m3s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m33s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m25s
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 / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m13s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m18s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m51s
AI Code Review / AI Code Review (pull_request) Successful in 6m39s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 51s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m10s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Unit Tests (push) Successful in 14m54s
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 / Integration Tests (push) Successful in 5m19s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m16s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m44s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 16:51:04 +08:00
xiaoxia 2827bf5943 fix: 移除 previewTaskId 自动清除 + 从预览响应提取 source_edit_plan_id (#1483)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m8s
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 / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m54s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m57s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m4s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m32s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m22s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m5s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 16:46:01 +08:00
xiaoxia 7fdea8301c fix: source_edit_plan_id 为空时标记任务为 failed 而非卡在 running (#1481)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 45s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m50s
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 3m8s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m15s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m44s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
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 / 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 / Build Staging API Image (push) Successful in 5m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
AI Code Review / AI Code Review (pull_request) Failing after 5m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m17s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m30s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m46s
CI/CD Pipeline / Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m23s
CI/CD Pipeline / Unit Tests (push) Successful in 14m25s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m31s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 16:06:00 +08:00
xiaoxia 726fb225c8 fix: Step7 确认生成优先调用 confirmGeneration 复用预览产物,解决卡 10% 问题 (#1480)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 34s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m39s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m43s
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) (pull_request) Successful in 2m31s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m56s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 52s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m16s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m25s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m17s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / Integration Tests (push) Successful in 6m52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 7m0s
CI/CD Pipeline / Unit Tests (push) Successful in 16m41s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 17m11s
CI/CD Pipeline / CI Gate (pull_request) Successful in 23s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 15:31:00 +08:00
xiaoxia c873bb635f refactor: 统一渲染链路,预览即所得(-3370行) (#1479)
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 / Check if frontend-only change (pull_request) Successful in 55s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m51s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m59s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m6s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m12s
AI Code Review / AI Code Review (pull_request) Failing after 6m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 39s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 14m24s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m30s
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 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 14:16:00 +08:00
xiaoxia 73665f0d60 fix: 修复 CI 失败的 4 个单元测试 (#1478)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 49s
CI/CD Pipeline / Frontend Lint (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 / 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 / Build Staging Worker Image (push) Successful in 1m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m35s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m55s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m4s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m42s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
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
AI Code Review / AI Code Review (pull_request) Successful in 3m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m20s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m23s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m25s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m20s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m31s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m57s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m18s
CI/CD Pipeline / Unit Tests (push) Successful in 13m45s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m22s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 12:26:01 +08:00
xiaoxia 706b9057d6 fix: 3个bug修复 - flush/append_log/plan_id兜底 (#1476)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m11s
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m57s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m34s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m3s
AI Code Review / AI Code Review (pull_request) Failing after 3m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m29s
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 / PR Build Worker Image (pull_request) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m42s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m40s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m14s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Integration Tests (push) Successful in 4m49s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m46s
CI/CD Pipeline / Unit Tests (push) Failing after 14m58s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 12m38s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 11:26:00 +08:00
xiaoxia 9da2fae3dd fix: sourceEditPlanId 不再用模板ID冒充plan_id,改由后端兜底关联 (#1475)
CI/CD Pipeline / Check if frontend-only change (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 / Validate - Migration (alembic) (push) Successful in 1m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m51s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m1s
CI/CD Pipeline / Check if frontend-only change (pull_request) 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 / Build Staging API Image (push) Successful in 2m16s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m10s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m8s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 47s
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
AI Code Review / AI Code Review (pull_request) Successful in 8m5s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m26s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m45s
CI/CD Pipeline / Integration Tests (push) Successful in 6m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m45s
CI/CD Pipeline / Unit Tests (push) Failing after 18m7s
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
CI/CD Pipeline / Unit Tests (pull_request) Failing after 15m11s
CI/CD Pipeline / CI Gate (pull_request) Failing after 32s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 10:24:54 +08:00
xiaoxia b19bfaa34d fix: sourceEditPlanId 不再用模板ID冒充plan_id,改由后端兜底关联 (#1475)
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) (pull_request) Successful in 1m8s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m10s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m37s
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 / 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 / Validate - Type Check (mypy) (push) Successful in 2m50s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m27s
AI Code Review / AI Code Review (pull_request) Successful in 3m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m58s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m56s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
2026-08-24 10:17:45 +08:00
xiaoxia 61770cd4e4 fix(worker): render_plan 保留视频原声并尊重每clip音量 (#1474)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m12s
CI/CD Pipeline / Frontend Lint (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 / 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 1m40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m9s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m28s
AI Code Review / AI Code Review (pull_request) Failing after 2m17s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
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 / Build Staging Web Image (push) Successful in 4m34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m38s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m46s
CI/CD Pipeline / Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Failing after 13m33s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 11m30s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 10s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-24 01:16:00 +08:00
xiaoxia 024aca3557 fix: source_edit_plan_id 正确传递 + clips时长与预览一致 (#1473)
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 / Check if frontend-only change (pull_request) Successful in 1m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m48s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m8s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m13s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 46s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m34s
AI Code Review / AI Code Review (pull_request) Successful in 5m23s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m13s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m50s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m47s
CI/CD Pipeline / Unit Tests (push) Failing after 17m39s
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
CI/CD Pipeline / Unit Tests (pull_request) Failing after 15m8s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-23 23:46:00 +08:00
xiaoxia bbf27ec3f6 chore: 清理剪辑模板编辑器生成视频死代码 + 重命名为剪辑模板 (#1472)
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 1m22s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m37s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m59s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m59s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m0s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m7s
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) (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m42s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 25s
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 / Build Staging API Image (push) Successful in 6m29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m17s
AI Code Review / AI Code Review (pull_request) Failing after 7m31s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m58s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m2s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m23s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m43s
CI/CD Pipeline / Integration Tests (push) Successful in 3m54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Unit Tests (push) Failing after 13m47s
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
CI/CD Pipeline / Unit Tests (pull_request) Failing after 12m35s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
2026-08-23 23:06:28 +08:00
xiaoxia febb1bcfce chore: 清理剪辑模板编辑器生成视频死代码 + 重命名为剪辑模板 (#1472)
CI/CD Pipeline / Check if frontend-only change (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 / Build Staging Worker Image (push) Successful in 1m2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) 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 / 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
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
前端:
- 删除 GenerationProgressModal/GenerationHistoryModal 组件及目录
- 删除 generateEditPlan/getGenerationStatus/getEditPlanGenerations 三个死API函数
- 删除对应类型 GenerateResponse/EditPlanGeneration/GenerationStatusResponse
- 清理测试文件中的 mock 和引用
- 用户可见文案 '剪辑编辑器' 统一改为 '剪辑模板'
- 不改变量名/组件名/路由路径/API路径

后端:
- 删除 templates_editor/generation.py 死接口文件
- 清理 _fallback.py、dependencies.py、schemas.py 中的死代码
- 删除对应无用测试文件
2026-08-23 23:05:15 +08:00
xiaoxia 9fd1246c6b fix(worker): P1 修复任务状态覆盖 + 封面metadata属性名错误 (#1471)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) 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 / 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 / Build Staging Worker Image (push) Successful in 1m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m21s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 33s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m19s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m38s
AI Code Review / AI Code Review (pull_request) Successful in 6m7s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m47s
CI/CD Pipeline / Integration Tests (push) Successful in 3m26s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m55s
CI/CD Pipeline / Unit Tests (push) Failing after 13m5s
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
CI/CD Pipeline / Unit Tests (pull_request) Failing after 11m56s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-23 19:11:01 +08:00
xiaoxia 392a20002c feat: Step2选素材同步写入edit_plan_clips (#1469)
CI/CD Pipeline / Check if frontend-only change (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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m12s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m12s
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 1m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m57s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m43s
AI Code Review / AI Code Review (pull_request) Failing after 2m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m18s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m8s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m11s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m9s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m49s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m41s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 3m38s
CI/CD Pipeline / Unit Tests (push) Failing after 13m10s
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
CI/CD Pipeline / Unit Tests (pull_request) Failing after 12m20s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 9s
2026-08-23 17:42:26 +08:00
xiaoxia 42744241c4 fix(worker): P0 修复@celery_app.task装饰器错位导致所有生成任务崩溃 (#1470)
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 58s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m18s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m36s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m59s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m3s
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) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m19s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
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 / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m31s
AI Code Review / AI Code Review (pull_request) Failing after 4m13s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m22s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m59s
CI/CD Pipeline / Unit Tests (push) Failing after 13m15s
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 / Unit Tests (pull_request) Failing after 11m37s
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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-23 17:16:00 +08:00
xiaoxia ab40c57e9e feat: sync selected materials to edit_plan_clips on Step2 (PR #1469)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 51s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m16s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m57s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m55s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m43s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m50s
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 Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m15s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m49s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m32s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m5s
AI Code Review / AI Code Review (pull_request) Successful in 8m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m13s
CI/CD Pipeline / Integration Tests (push) Successful in 3m11s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m20s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Successful in 12m25s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m2s
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 9s
- New API updateEditPlanClips (PUT /templates/{id}/editor/clips)
- useStep2Materials: debounced 800ms clips write, same duration
  logic as FrontendPreviewPlayer.buildPlaybackSegments
- buildClipsFromAssets shared util ensures preview/DB consistency
- Supports manual + smart-match modes, silent failure
2026-08-23 16:25:50 +08:00
xiaoxia b4e3bb0fe7 refactor(worker): generate_video直接读取edit_plan_clips渲染 (#1468)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 47s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 50s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m27s
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 3m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m21s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m19s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m25s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m21s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Unit Tests (push) Successful in 15m5s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m25s
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 9s
2026-08-23 16:03:30 +08:00
xiaoxia 4de4b8dc08 fix(cover): E2兜底封面标题样式透传color/position/font_size (#1467)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m30s
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m53s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m37s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
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
AI Code Review / AI Code Review (pull_request) Successful in 3m23s
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m14s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m18s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m31s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m9s
CI/CD Pipeline / Integration Tests (push) Successful in 3m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m45s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m28s
CI/CD Pipeline / Unit Tests (push) Successful in 14m12s
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 / 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 / Unit Tests (pull_request) Successful in 11m29s
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 9s
2026-08-23 14:57:05 +08:00
xiaoxia 622e9742f5 fix(edit-flow): 批量生成 plan.config 只回写一次 (#1466)
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 / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m42s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m10s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m30s
AI Code Review / AI Code Review (pull_request) Failing after 3m34s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m14s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m46s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m53s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m56s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m56s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m11s
CI/CD Pipeline / Integration Tests (push) Successful in 2m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m39s
CI/CD Pipeline / Unit Tests (push) Successful in 14m18s
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 / 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 / Unit Tests (pull_request) Successful in 11m44s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 10s
2026-08-23 14:19:16 +08:00
xiaoxia ab381b2e74 feat(generate): 编辑流程自动保存草稿+创建任务参数修复+轮询改新接口+清理幽灵API (#1465)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m2s
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 / Build Staging Worker Image (push) Successful in 1m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m45s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m39s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m35s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m18s
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 Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 2m37s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m32s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 25s
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 / Build Staging API Image (push) Successful in 6m21s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m6s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m44s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m36s
CI/CD Pipeline / Integration Tests (push) Successful in 3m33s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m6s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m25s
CI/CD Pipeline / Unit Tests (push) Successful in 13m14s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m47s
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 46s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-23 13:56:32 +08:00
xiaoxia 94caa63436 Merge pull request 'fix(edit-flow): 补全 title_config 数据链路,修复编辑流程断裂' (#1464) from fix/edit-flow-data-chain into develop
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
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-23 13:55:42 +08:00
xiaoxia fe49fef1ad fix: auto-save draft + API cleanup for editing flow (PR #1465)
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 55s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m12s
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 / 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- Step2/Step4 auto-save via useDraftAutoSave (GET-merge-PUT, serial queue)
- useDraftAutoSave: failed saves preserve patch with exponential backoff retry
- Step6 cover requests include full title_config
- Step7 create task: source_edit_plan_id, title_config, cover_url priority
- Polling switched to GET /generation/tasks/{task_id} with proper error handling
- getGenerationTaskResults retries before treating as failure
- Removed 7 ghost API functions (non-existent backend routes)
- BGM presets path updated to /templates/{id}/editor/bgm/presets
- Type alignment for BatchGenerationTaskResponse
2026-08-23 13:53:36 +08:00
CI Bot 0b000f96a6 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 45s
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 41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m50s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m46s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m18s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m17s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m6s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Waiting to run
AI Code Review / AI Code Review (pull_request) Waiting for AI Code Review to complete
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m13s
2026-08-23 05:39:27 +00:00
CI Bot 817c6fa6a3 test: 补全 _writeback_edit_plan_config 单测,修复增量覆盖率不足
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 35s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m39s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m20s
CI/CD Pipeline / PR Build Web 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
AI Code Review / AI Code Review (pull_request) Failing after 2m16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m8s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 4m18s
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
新增 9 个单元测试覆盖 generation_tasks.py 增量代码:
- 空 plan_id 直接返回
- plan 不存在 warning + 不 commit
- 正常写入(有/无 title_config)
- config 非 dict 兜底
- DB 异常 rollback + rollback 失败静默
- 空 title_config 不写入
2026-08-23 13:34:57 +08:00
CI Bot 410f672195 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 37s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m21s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m48s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m15s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m50s
AI Code Review / AI Code Review (pull_request) Successful in 4m53s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m1s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m40s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 12m4s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
2026-08-23 05:15:40 +00:00
agent 06f68230af fix(edit-flow): 补全 title_config 数据链路,修复编辑流程断裂
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 32s
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 55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m1s
AI Code Review / AI Code Review (pull_request) Failing after 2m15s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m1s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
1. Schema: CreateGenerationTaskRequest 增加 title_config(dict|None=None)
2. DB: generation_tasks 新增 title_config JSON 列 (alembic 057)
3. Domain/ORM/Repo/UseCase 全链路透传 title_config
4. API 创建任务成功后,merge 回写 EditPlan.config
   (generation_task_id + title_config),失败不影响主流程
5. Worker 优先使用 title_config 构建标题图层,custom_title 兜底
6. GenerationTaskResponse 返回 title_config
7. 旧路由 /generate 和 /generation-status 标记 DEPRECATED

所有新字段默认 None/{},不传时行为与现在完全一致。
2026-08-23 13:12:40 +08:00
xiaoxia 5a5c653d2c feat(cleanup): pending 任务自动清理,防止队列占满 (#1463)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m13s
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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) 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 2m8s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m17s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m19s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m41s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m4s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m38s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m46s
CI/CD Pipeline / Integration Tests (push) Successful in 2m51s
AI Code Review / AI Code Review (pull_request) Successful in 8m5s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m9s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Successful in 13m12s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m19s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
2026-08-23 12:55:39 +08:00
CI Bot 2148e2bc48 chore: trigger CI rebuild after api-base image update (Pillow + CJK fonts)
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 / Check if frontend-only change (pull_request) Successful in 1m3s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m25s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m26s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m23s
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 / Build Staging Web Image (push) Successful in 5m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m30s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m32s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m48s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m54s
AI Code Review / AI Code Review (pull_request) Successful in 8m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m39s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m6s
CI/CD Pipeline / Unit Tests (push) Successful in 14m38s
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 / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m14s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 46s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-23 11:13:41 +08:00
xiaoxia 9ac21d37f0 Merge pull request 'fix(cover): Worker本地ffmpeg抽帧 + E2源素材封面标题叠加' (#1462) from fix/cover-ffmpeg-title-overlay into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 48s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m29s
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 2m48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m48s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m8s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m33s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m18s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m7s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m13s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
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 / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m56s
AI Code Review / AI Code Review (pull_request) Successful in 8m30s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m30s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m47s
CI/CD Pipeline / Integration Tests (push) Successful in 5m8s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m37s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m41s
CI/CD Pipeline / Unit Tests (push) Successful in 14m40s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m57s
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 19s
API Base Image Build / Build API Base Image (push) Successful in 29m19s
Worker Base Image Build / Build Worker Base Image (push) Successful in 33m30s
2026-08-23 10:43:40 +08:00
CI Bot f6b50b49ec fix(cover): worker local ffmpeg cover frames + title overlay for source-asset fallback
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m39s
AI Code Review / AI Code Review (pull_request) Successful in 4m45s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m23s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m39s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m23s
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 15s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 52s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m31s
问题:
1. 之前 Worker 用 MediaKit 网络抽帧写 cover_url,因状态字符串 bug(#1461 已修)
   一直失败;且在 API 请求内同步抽帧易超时。
2. 封面从源素材抽取时无标题(源素材未烧录标题)。

修复:
1. Worker 渲染完成后直接复用 RenderAdapter 已用本地 ffmpeg 抽出的 cover_candidates:
   - _render_video 返回值改为 (output_path, render_duration, cover_candidates)
   - 第一帧写入 gen_task.cover_url,完整列表写入 metadata.cover_candidates
   - 删除步骤 4.5 的 MediaKit 抽帧代码(不依赖网络,不阻塞)
2. 标题叠加:
   - 新增 packages/shared/title_overlay.py,Pillow 绘制白色文字+黑色描边/阴影,
     支持 CJK 字体和自动换行(API/Worker 共用)
   - thumbnail_generator.extract_and_upload_cover_frames 的 title_text 参数真正生效
   - render_adapter 从已渲染视频抽帧时传空 title_text(标题已 ASS 烧录,避免重影)
   - 封面 API E2 从源素材抽帧后,从 plan.config.title.text 读取标题并叠加
3. API 基础镜像增加 Pillow 和 fonts-noto-cjk(E2 标题叠加依赖)

测试:新增 test_title_overlay.py(5)、E2 标题透传测试,更新 1294 解包;
相关 51 passed。
2026-08-23 10:28:52 +08:00
xiaoxia 2837f11123 fix: 预览完成后保存rendered_storage_key + createPreview传title_config
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m26s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m18s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m20s
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 Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 51s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m47s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 47s
AI Code Review / AI Code Review (pull_request) Failing after 4m51s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m48s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m35s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m9s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m40s
CI/CD Pipeline / Integration Tests (push) Successful in 4m36s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m49s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m24s
CI/CD Pipeline / Unit Tests (push) Successful in 16m33s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 15m37s
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 13s
2026-08-23 10:19:28 +08:00
xiaoxia ea42e48a9f Merge pull request 'fix(mediakit): 抽帧轮询识别 completed 状态,修复封面抽帧始终超时失败' (#1461) from fix/mediakit-completed-status into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m56s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m57s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m17s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
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
AI Code Review / AI Code Review (pull_request) Successful in 3m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m14s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m34s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m12s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m53s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m25s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m27s
CI/CD Pipeline / Integration Tests (push) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 14m7s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m28s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
2026-08-23 01:02:24 +08:00
CI Bot 39683bda09 fix(mediakit): accept "completed" status in extract_frames polling
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 37s
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 49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m38s
AI Code Review / AI Code Review (pull_request) Successful in 1m40s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m56s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m25s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m41s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m53s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m52s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m15s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 41s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 43s
火山引擎 MediaKit 抽帧 API 完成时返回 status="completed",
但 _poll_task_result_with_error 只判断 "success",
导致任务实际已成功返回帧数据,客户端却继续轮询到超时返回 None,
封面抽帧一直失败。

与同文件 _poll_video_understand_result_with_error 的判断保持一致。
Worker 渲染后抽帧和 API E1/E2 兜底均受此 bug 影响。
2026-08-23 00:51:18 +08:00
xiaoxia b3aa05e510 Merge pull request 'fix(cover): 封面 API MediaKit 抽帧短超时,避免请求挂死' (#1460) from fix/cover-mediakit-timeout into develop
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m25s
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 2m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m28s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m27s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m10s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m28s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m56s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m40s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m46s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m10s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m13s
AI Code Review / AI Code Review (pull_request) Successful in 7m51s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m38s
CI/CD Pipeline / Integration Tests (push) Successful in 3m36s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Unit Tests (push) Successful in 13m12s
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 / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m10s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
2026-08-23 00:15:05 +08:00
CI Bot 005f500ee1 fix(cover): short MediaKit polling timeout in API cover endpoint (E1/E2)
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 50s
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 39s
AI Code Review / AI Code Review (pull_request) Successful in 1m36s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m45s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m51s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / 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 2m3s
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 57s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 1m32s
E1/E2 fallback calls used default 30 polling attempts * 2s = 60s each,
causing the cover API to hang ~2 minutes when MediaKit is slow/unreachable.
Set max_poll_attempts=5, poll_interval=2, max_retries=0 for API-side
fallback so it fails fast (~10s) instead of blocking the request.

Worker-side extraction keeps default timeout (background task, safe).
2026-08-23 00:03:52 +08:00
xiaoxia a11178eb13 fix(cover): extract cover from source asset when no backend preview exists (#1459)
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 / Check if frontend-only change (pull_request) Successful in 1m1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m16s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m13s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m37s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m39s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m20s
AI Code Review / AI Code Review (pull_request) Failing after 4m50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m53s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m19s
CI/CD Pipeline / Integration Tests (push) Successful in 3m29s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m29s
CI/CD Pipeline / Unit Tests (push) Successful in 14m27s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m42s
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 8s
2026-08-22 23:15:33 +08:00
xiaoxia 4a6d84921d fix(cover): add plan.config.cover_candidates fallback in cover generation endpoint (#1458)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 53s
CI/CD Pipeline / Frontend Lint (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 / Build Staging Worker Image (push) Successful in 1m24s
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) (pull_request) Successful in 1m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m29s
AI Code Review / AI Code Review (pull_request) Successful in 2m32s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m56s
CI/CD Pipeline / PR Build Web 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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m12s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m27s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m36s
CI/CD Pipeline / Unit Tests (push) Successful in 14m5s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m32s
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 8s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-22 21:51:01 +08:00
xiaoxia 98cd806d29 style: 封面预览框缩小至max-width 180px居中
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m30s
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m48s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
AI Code Review / AI Code Review (pull_request) Failing after 2m54s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m14s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m31s
CI/CD Pipeline / Integration Tests (push) Successful in 5m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m35s
CI/CD Pipeline / Unit Tests (push) Successful in 17m5s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m48s
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 9s
2026-08-22 20:34:52 +08:00
xiaoxia 6a57431f68 fix: 配音素材时长/大小显示为0 — fallback读取metadata字段
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 59s
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 / 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 1m47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m54s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m54s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m39s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m21s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m22s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m23s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m0s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Successful in 14m39s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m17s
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 9s
2026-08-22 18:41:18 +08:00
xiaoxia d19fa951ee 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 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 API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m52s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m41s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 52s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m26s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m59s
AI Code Review / AI Code Review (pull_request) Failing after 6m6s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m34s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m12s
CI/CD Pipeline / Integration Tests (push) Successful in 3m46s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m44s
CI/CD Pipeline / Unit Tests (push) Successful in 14m31s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m37s
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 8s
2026-08-22 18:23:46 +08:00
xiaoxia e928068efd refactor: v3调度器——所有状态存入ref彻底消除RAF闭包陷阱+片段切换先播下一段再切可见性
CI/CD Pipeline / Check if frontend-only change (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 / Build Staging Worker Image (push) Successful in 1m2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m29s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m33s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m13s
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) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m11s
AI Code Review / AI Code Review (pull_request) Successful in 6m42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m50s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m5s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / Integration Tests (push) Successful in 4m18s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m7s
CI/CD Pipeline / Unit Tests (push) Successful in 13m49s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m3s
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 9s
2026-08-22 15:35:37 +08:00
xiaoxia 18371dc438 fix: 修复RAF循环穿透导致指数级增长+音频同步重置卡顿
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m20s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m54s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m3s
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) (pull_request) Successful in 2m6s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m56s
AI Code Review / AI Code Review (pull_request) Successful in 5m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m23s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m21s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m8s
CI/CD Pipeline / Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Unit Tests (push) Successful in 13m23s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m3s
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 8s
useSegmentScheduler.ts:
- 新增 tickRef 保存最新 tick 引用,所有 RAF 调度通过 () => tickRef.current()
- 片段边界切换分支加 return,阻止旧闭包穿透到末尾重复调度 RAF
  (这是网页越来越卡的根因:每次片段切换 RAF 链数量翻倍)
- play() 和重播分支的 RAF 同样改用 tickRef

FrontendPreviewPlayer.tsx:
- 片段切换音频同步 useEffect 移除 currentTime 依赖
  原依赖每200ms变化导致 audio.currentTime 被反复重置,音频卡顿
2026-08-22 13:38:02 +08:00
xiaoxia 4e9c9a2eda fix: 禁用useCanvasPlayer后台解码+恢复所有video预加载
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 / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m32s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m2s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m19s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m25s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 41s
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 / PR Build Worker Image (pull_request) Successful in 36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m15s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m43s
AI Code Review / AI Code Review (pull_request) Successful in 7m52s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m9s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Integration Tests (push) Successful in 5m32s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m4s
CI/CD Pipeline / Unit Tests (push) Successful in 15m32s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m19s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
- useCanvasPlayer 新增 enabled 参数,enabled=false 时 hasSupport=false
  且 init useEffect 直接 return,彻底不执行 WebCodecs 初始化和后台解码
- FrontendPreviewPlayer 传入 enabled=(useWebCodecs && !forceVideoFallback),
  useWebCodecs=false 时 useCanvasPlayer 完全不启动,消除网页卡顿元凶
- 恢复所有 video 元素 preload=auto + src,撤掉条件预加载逻辑
  (之前条件预加载导致 switchToSegment 时频繁动态设 src,影响播放和进度)
2026-08-22 02:37:46 +08:00
xiaoxia b8a349d5e0 refactor: 默认使用原生video播放,彻底解决主线程解码卡顿
CI/CD Pipeline / Check if frontend-only change (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 / Build Staging Worker Image (push) Successful in 49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
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 3m27s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m29s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 50s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m25s
AI Code Review / AI Code Review (pull_request) Successful in 8m47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 11m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 15m5s
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 6m45s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m53s
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
FrontendPreviewPlayer.tsx:
- useWebCodecs 默认 false,走浏览器原生硬件解码(独立线程)
- 删除 Canvas ResizeObserver useEffect(默认路径不需要)
- video 元素只预加载当前+下一片段,其余 preload=none+不设src,减少并发连接
- 片段指示器统一显示片段序号,不再显示Canvas/降级提示
- isWebCodecsSupported 不再导入,清理 TS6133

useSegmentScheduler.ts:
- tick 进度更新 200ms 节流(5fps),避免 60fps setCurrentTime 触发整树 re-render
- 重播/seek 时重置节流 ref
- switchToSegment 兜底:若 video 无 src 则动态设置并 load(),兼容 seek 到未预加载片段

useCanvasPlayer.ts 不删,保留作为兜底
2026-08-22 01:53:43 +08:00
xiaoxia 9c02e69ede fix: 第13轮 — 背压阈值20→64+移除idleTicks超时+等待10ms→5ms
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 / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m46s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
AI Code Review / AI Code Review (pull_request) Failing after 5m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m51s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Unit Tests (push) Successful in 14m38s
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 5m7s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m3s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m11s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 10s
- 背压阈值从20提到64,给硬件解码器足够流水线深度
- 等待间隔10ms→5ms,减少轮询开销
- 删除idleTicks 3秒超时,帧队列容量(180)才是真正背压
- 保留帧输出等待(500ms)和完成日志
2026-08-22 01:09:59 +08:00
xiaoxia ece1f25f71 fix: 第12轮 — 背压超时保护+帧输出等待+feedFrames阈值
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m11s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m24s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m49s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m32s
AI Code Review / AI Code Review (pull_request) Failing after 5m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (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
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
- 背压等待加 idleTicks 计数,超3秒自动break防止死循环
- 解码完成后等待帧输出(最多500ms),确保队列有帧
- 加解码完成日志便于排查
- renderFrame 中 feedFrames 调用加 size<80 阈值保护
2026-08-22 00:57:38 +08:00
xiaoxia 2885ede533 fix: 第11轮 — 解码流水线批量提交+背压+移除每帧log
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m16s
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 / Build Staging Worker Image (push) Successful in 1m20s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m26s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m36s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m51s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m43s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m3s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m17s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m28s
AI Code Review / AI Code Review (pull_request) Successful in 9m6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m11s
CI/CD Pipeline / Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m36s
CI/CD Pipeline / Unit Tests (push) Successful in 13m41s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m50s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
- decodeSegmentBatch: 移除每帧 await+3s超时,改为批量提交 decode()
- 添加 decodeQueueSize>20 背压,防止解码器内部队列积压
- 批量提交后 16ms 等待让解码器输出帧
- feedFrames 缓冲阈值 100→80,减少后台解码抢占渲染
- 删除 drawTitle 每帧 console.log(JSON.stringify 开销导致抖动)
2026-08-22 00:15:27 +08:00
xiaoxia ac31c88c96 fix: 修复 TS2367 类型收窄错误 — decoder.state 显式断言为 string
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 / Check if frontend-only change (pull_request) Successful in 1m3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m58s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m54s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m4s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m0s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m50s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m3s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m9s
AI Code Review / AI Code Review (pull_request) Failing after 8m20s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m24s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m42s
CI/CD Pipeline / Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Successful in 14m24s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m11s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
2026-08-21 23:43:15 +08:00
xiaoxia 585a3b6887 fix: 第10轮方案A — 滑动窗口+持久解码器+二分查找
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m16s
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 / 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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m6s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m6s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m40s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 2m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m31s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 7m21s
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
AI Code Review / AI Code Review (pull_request) Successful in 7m47s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m17s
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
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
架构级重构,从根本解决抖动/卡顿/黑屏/重播失效/关键帧错误:

- FrameQueue 默认容量 200,getCurrentFrame 改二分查找 O(log n)
- 每个片段持有持久化 VideoDecoder,连续喂 sample 不报关键帧错误
- segmentSampleCursorRef 记录游标,支持断点续解码
- feedFrames 后台补充:队列低于 100 帧自动续解,保持 ≥6s 缓冲
- init 只解前 2 个片段各 60 帧,播放中后台补充
- 重播/seek:关闭旧解码器→清空队列→重置游标→重新解码
- isFeedingRef 防重入
2026-08-21 23:31:44 +08:00
xiaoxia 412aa83b32 fix: 第9轮最终简化方案 — init全量解码+播放时只渲染
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 / Check if frontend-only change (pull_request) Successful in 1m21s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m8s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m14s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m17s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 53s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m0s
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
AI Code Review / AI Code Review (pull_request) Failing after 3m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m33s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m6s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m51s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m18s
CI/CD Pipeline / Unit Tests (push) Successful in 14m31s
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 5m2s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m14s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
根因:之前 init 只解 60 帧,播放中续解码导致关键帧错误、
重复帧、队列混乱、抖动。

新方案:
- init 阶段一次性解码全部 894 帧(队列 1024 足够容纳)
- 播放时只做 drawImage,不触发任何解码
- 重播直接复用已有帧,不重新解码
- seek 直接定位,不清帧队列
- 删除 decodeAroundPosition 动态解码函数
2026-08-21 22:16:22 +08:00
xiaoxia c6eeca75fb fix: 删除 decodeSegment 的 startFrame 参数,修复关键帧错误
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 / Check if frontend-only change (pull_request) Successful in 50s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m1s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m45s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m56s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m20s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 42s
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 / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 3m34s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m20s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m3s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m1s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Unit Tests (push) Successful in 14m50s
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 4m51s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m49s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m22s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
WebCodecs 解码器要求第一个 chunk 必须是关键帧。续解码从 startFrame=60
开始时,samplesCollected[60] 不是关键帧 → 报错 "A key frame is required"。

修复:
- 删除 startFrame 参数,sample 循环永远从 0 开始
- decodeAroundPosition 不再传 startFrame
- totalDecoded 计算不再使用 startFrame 偏移
2026-08-21 21:25:19 +08:00
xiaoxia df4de37f73 fix: MAX_INIT_FRAMES 500 → 60,减少 init 阶段解码帧数降低播放抖动
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m1s
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 / 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 1m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m27s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 57s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m59s
AI Code Review / AI Code Review (pull_request) Failing after 5m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m30s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m4s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m24s
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
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
init 阶段解码 894 帧导致 getCurrentFrame 遍历过多帧,播放抖动。
改为每片段只解 60 帧(≈2s),播放时由 decodeAroundPosition 按需续解。
2026-08-21 21:11:08 +08:00
xiaoxia 88c07ef142 fix: 视频 fallback 路径片段切换卡顿 — 预加载策略升级
CI/CD Pipeline / Check if frontend-only change (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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
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 1m46s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m31s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m4s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m9s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m26s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 52s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 14m29s
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
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
根因:UseWebCodecs=false 时走原生 video fallback,preload 策略为
当前片段 auto / 下一片段 metadata / 其余 none,导致切换时下一片段
需现场从网络缓冲,卡顿约 10 秒。

修复:
1. 所有 video 元素 preload="auto",让浏览器提前缓冲全部片段
2. tick 循环中距出点 ≤3s 时主动 seek 下一片段触发缓冲
3. switchToSegment 超时从 10s 降到 3s(预加载正常时无需久等)
2026-08-21 20:55:36 +08:00
xiaoxia 0996ba45ec fix: 续解码架构 + 帧队列扩容 1024 + 重播/seek/destroy 同步帧计数
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m15s
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 / 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 2m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m39s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m10s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m1s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m10s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m4s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m20s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m5s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m15s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Unit Tests (push) Successful in 13m46s
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 4m57s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m2s
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 4m6s
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
2026-08-21 20:30:45 +08:00
xiaoxia a34639975c fix: 增大 init 解码上限 + 重播不清帧队列 + sample 循环 generation 检查
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 57s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m7s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m21s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m46s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
AI Code Review / AI Code Review (pull_request) Failing after 3m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m9s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m39s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m50s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m3s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Unit Tests (push) Successful in 14m33s
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 5m4s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m52s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m10s
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 8s
2026-08-21 19:58:40 +08:00
xiaoxia 4b68c9be9c fix: 给 decoder.decode() 加 3s 超时保护,防止解码卡死
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 / Check if frontend-only change (pull_request) Successful in 1m23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m43s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m1s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m50s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m6s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m42s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m7s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Unit Tests (push) Successful in 13m51s
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 / Unit Tests (pull_request) Successful in 11m49s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m10s
CI/CD Pipeline / CI Gate (pull_request) Successful in 10s
CI/CD Pipeline / Integration Tests (push) Successful in 5m50s
CI/CD Pipeline / CI Gate (push) Has been skipped
2026-08-21 19:09:39 +08:00
xiaoxia fbe0a6e826 fix: 视频播放结束后重播时重置 currentTime 到起点
CI/CD Pipeline / Check if frontend-only change (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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m16s
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 3m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m42s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m48s
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 4m12s
AI Code Review / AI Code Review (pull_request) Failing after 3m28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m7s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 57s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 59s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m21s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 11m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 15m21s
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 / Build Production API 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 5m50s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m2s
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 5m44s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
2026-08-21 17:18:35 +08:00
xiaoxia 7f2861e98e fix: 移除 decoder.flush() 调用,修复 Chromium 下 flush 永久挂起导致解码卡死
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m57s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m43s
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) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m33s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m59s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m53s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m52s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 55s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Unit Tests (push) Successful in 14m28s
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
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m13s
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 5m47s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
2026-08-21 16:57:20 +08:00
xiaoxia b1aa866252 fix: 修复 flush 超时 catch 块类型错误导致 CI 构建失败
CI/CD Pipeline / Check if frontend-only change (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 / Check if frontend-only change (pull_request) Successful in 59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m11s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m14s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-21 16:53:53 +08:00
xiaoxia f484de79c4 ci: trigger web build 1787302071
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Failing after 2m37s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m41s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m53s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 2m1s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 47s
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 4m30s
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 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
2026-08-21 16:47:51 +08:00
xiaoxia 9263db56db ci: retry build
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 57s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m13s
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 2m53s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m24s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 4m4s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 5m24s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m26s
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 / Validate - Type Check (mypy) (pull_request) Successful in 4m34s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 1m46s
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
2026-08-21 16:41:53 +08:00
xiaoxia ebe43f163b chore: DIAG_v2 诊断日志 + flush 5s 超时保护
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 36s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m33s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m40s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m39s
AI Code Review / AI Code Review (pull_request) Failing after 2m20s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m2s
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
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 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 / 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 / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 36s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m21s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 4m51s
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 / Frontend Unit Tests (push) Successful in 4m34s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build 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
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
2026-08-21 16:30:59 +08:00
xiaoxia 3e38395f46 chore: 添加 [DIAG] 诊断日志定位 decode loop 卡死点
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m34s
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 (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m30s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m52s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m43s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m56s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m6s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 4m3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 41s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m2s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 7m58s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 36s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m2s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 18m29s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 22m3s
CI/CD Pipeline / Unit Tests (push) Failing after 22m9s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 22m10s
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 / 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 (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) 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 (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 7m39s
CI/CD Pipeline / Integration Tests (push) Successful in 7m41s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 10s
2026-08-21 15:43:16 +08:00
xiaoxia 9cb20c38ae fix: Strict Mode 双执行导致 decode loop 提前退出
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 41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m12s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m3s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m31s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m28s
CI/CD Pipeline / Unit Tests (push) Successful in 15m9s
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
React 18 Strict Mode 下 effect 执行两次(run → cleanup → run),
destroy() 和 init() 都调用 decodeGenerationRef.current++ 导致
generation 被多增两次,decode loop 内 generation check 误判为
seek/destroy 事件而提前退出。

修复:
- destroy() 移除 decodeGenerationRef.current++
- init() 移除 decodeGenerationRef.current++
- seek()/play() 保留 generation 递增不变
- decoder.state === 'closed' 时 return 而非 break,让第二轮
  init 重新创建 decoder 正常解码
2026-08-21 15:27:42 +08:00
xiaoxia d784af7f67 fix: 重置 isDestroyedRef 修复 effect 生命周期导致解码永久跳过
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 / Build Staging API Image (push) Successful in 2m44s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m49s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m58s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 54s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m38s
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 9m17s
CI/CD Pipeline / CI Gate (push) Has been skipped
useEffect cleanup 调用 destroy() 将 isDestroyedRef 设为 true,
但新的 init 周期不重置它,导致 loadSegment/decodeSegment 立即 return,
isReady 永远为 false → canPlay 为 false → '暂无可播放素材'

修复:
- init 开始时重置 isDestroyedRef = false + 递增 decodeGeneration
- 同步重置 hasDecodeError/errorMessage/isReady 状态
- decode 循环内每次迭代后增加 cancelled 检查
- 增加诊断日志(decode loop 前后 + cancelled 分支)
2026-08-21 15:01:17 +08:00
xiaoxia 1b6ecc39f0 fix: WebCodecs 解码失败时自动 fallback 到原生 video 播放
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 2m37s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m52s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Unit Tests (push) Successful in 13m30s
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 5m7s
CI/CD Pipeline / CI Gate (push) Has been skipped
- useCanvasPlayer 新增 hasDecodeError/errorMessage 状态和 onError 回调
- decodeSegment catch 块不再静默失败,更新状态并通知上层
- VideoDecoder error 回调同步报告错误状态
- 连续 3 次解码失败时报告 hasDecodeError
- FrontendPreviewPlayer 检测 hasDecodeError 后自动切换 video fallback
- 解码失败时显示具体错误信息而非静默'暂无可播放素材'
- 新增 normalizeCodecString 函数规范化 codec 字符串
- configure 调用增加 codec 字符码调试日志
2026-08-21 14:43:12 +08:00
134 changed files with 26829 additions and 8195 deletions
@@ -0,0 +1,26 @@
"""Add title_config to generation_tasks
Revision ID: 057_title_config
Revises: 056_fix_cover_templates_config
Create Date: 2026-08-23
"""
import sqlalchemy as sa
from alembic import op
revision = "057_title_config"
down_revision = "056_fix_cover_templates_config"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"generation_tasks",
sa.Column("title_config", sa.JSON(), nullable=False, server_default="{}"),
)
def downgrade() -> None:
op.drop_column("generation_tasks", "title_config")
+224 -39
View File
@@ -31,8 +31,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["Generation"]) router = APIRouter(tags=["Generation"])
# ── Schemas ────────────────────────────────────────────────────────────── # ── Schemas ──────────────────────────────────────────────────────────────
@@ -65,6 +63,83 @@ class GenerateCoverResponse(BaseModel):
# ── Route ──────────────────────────────────────────────────────────────── # ── Route ────────────────────────────────────────────────────────────────
def _persist_cover_frame(
frame_url: str,
plan_id: str,
title_text: str = "",
*,
title_color: str = "#ffffff",
title_position: str = "bottom",
title_font_size: int | None = None,
) -> str:
"""下载 MediaKit 返回的临时帧图,可选叠加标题后转存到 OSS covers/ 路径。
Args:
frame_url: MediaKit 返回的临时帧图 URL
plan_id: 剪辑计划 ID(生成 OSS key
title_text: 非空时用 Pillow 在帧上叠加标题(用于 E2 从源素材抽帧,
因为源素材本身没有烧录标题)
title_color: 标题字体颜色(#RRGGBB
title_position: 标题位置 top/center/bottom
title_font_size: 标题字号,None 时自动计算
"""
import tempfile
import uuid
from pathlib import Path
tmp_path: str | None = None
try:
import httpx
resp = httpx.get(frame_url, timeout=30, follow_redirects=True)
resp.raise_for_status()
if not resp.content:
return frame_url
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
tmp.write(resp.content)
tmp_path = tmp.name
# E2 从源素材抽帧时,源素材无标题,叠加标题文字
if title_text and title_text.strip():
try:
from packages.shared.title_overlay import apply_title_to_image
applied = apply_title_to_image(
tmp_path,
title_text,
color=title_color,
position=title_position,
font_size=title_font_size,
)
if applied:
logger.info("[封面生成] E2 帧图已叠加标题: plan_id=%s", plan_id)
except Exception:
logger.warning(
"[封面生成] E2 标题叠加失败(返回无标题帧): plan_id=%s",
plan_id,
exc_info=True,
)
from packages.shared.storage import get_shared_storage_service
storage = get_shared_storage_service()
cover_key = f"covers/{plan_id}/cover_{uuid.uuid4().hex[:8]}.jpg"
storage.upload_file(
file_or_path=tmp_path,
storage_key=cover_key,
content_type="image/jpeg",
)
public_url = storage.get_url(cover_key)
return public_url or frame_url
except Exception:
logger.warning("封面帧转存失败,返回原始 URL: plan_id=%s", plan_id, exc_info=True)
return frame_url
finally:
if tmp_path:
Path(tmp_path).unlink(missing_ok=True)
@router.post("/generate-cover", response_model=GenerateCoverResponse) @router.post("/generate-cover", response_model=GenerateCoverResponse)
def generate_cover( def generate_cover(
body: GenerateCoverRequest, body: GenerateCoverRequest,
@@ -198,44 +273,31 @@ def generate_cover(
exc_info=True, exc_info=True,
) )
# 仍然找不到才报 400 # 使用裸 URLrendered/* 已配置公开读);找不到渲染视频时不立即报错,
if not rendered_storage_key: # 因为步骤 E 可以直接从源素材抽帧(历史数据或 Worker 抽帧失败时的兜底)
logger.error("[封面生成] ❌ 找不到预览视频: plan_id=%s", plan_id)
raise HTTPException(
status_code=400,
detail="请先生成预览视频,再生成封面",
)
# 回写到 plan.config
plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key})
# 使用裸 URLrendered/* 已配置公开读)
primary_video_url = None primary_video_url = None
try: if rendered_storage_key:
if rendered_storage_key.startswith("http"): plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key})
primary_video_url = rendered_storage_key try:
else: if rendered_storage_key.startswith("http"):
from packages.shared.storage import get_shared_storage_service primary_video_url = rendered_storage_key
else:
from packages.shared.storage import get_shared_storage_service
storage_svc = get_shared_storage_service() storage_svc = get_shared_storage_service()
primary_video_url = storage_svc.get_url(rendered_storage_key) primary_video_url = storage_svc.get_url(rendered_storage_key)
# 防御性规范化:合并路径中的双斜杠(// -> /),但保留协议头的 :// if primary_video_url:
# 历史数据中 project_id 为空时会产生 projects//tasks/ 路径, import re as _re
# MediaKit 的 HTTP 客户端会规范化 URL 导致 404
if primary_video_url:
import re as _re
primary_video_url = _re.sub(r"(?<!:)//", "/", primary_video_url) primary_video_url = _re.sub(r"(?<!:)//", "/", primary_video_url)
logger.info( logger.info(
"获取预览视频URL用于封面生成: plan_id=%s url=%s", "获取预览视频URL用于封面生成: plan_id=%s url=%s",
plan_id, plan_id,
primary_video_url[:80] if primary_video_url else "", primary_video_url[:80] if primary_video_url else "",
) )
except Exception as e: except Exception as e:
raise HTTPException( logger.warning("获取预览视频URL失败: plan_id=%s err=%s", plan_id, e)
status_code=500, primary_video_url = None
detail=f"获取预览视频URL失败: {e}",
) from e
# 统一封面管道:优先从 GenerationTask.cover_url 读取渲染后视频抽帧的封面 # 统一封面管道:优先从 GenerationTask.cover_url 读取渲染后视频抽帧的封面
# 多步查找 cover_url,和查找视频 URL 一样的 fallback 逻辑 # 多步查找 cover_url,和查找视频 URL 一样的 fallback 逻辑
@@ -310,6 +372,129 @@ def generate_cover(
exc_info=True, exc_info=True,
) )
# 步骤 D:从 plan.config.cover_candidates 读取(Worker 渲染时写入)
if not cover_url_from_task:
_candidates = (plan.config or {}).get("cover_candidates") or []
if isinstance(_candidates, list) and _candidates:
_first = _candidates[0]
if isinstance(_first, dict):
cover_url_from_task = _first.get("image_url") or _first.get("url") or ""
if cover_url_from_task:
logger.info(
"[封面生成] 统一管道封面(步骤D-cover_candidates): plan_id=%s url=%s",
plan_id,
cover_url_from_task[:80],
)
# 步骤 E1:如果有已渲染的预览视频 URL 但 cover_url 未持久化(历史数据),
# 直接从渲染视频抽帧
if not cover_url_from_task and primary_video_url:
try:
from packages.shared.mediakit_client import get_mediakit_client
mk_client = get_mediakit_client()
if mk_client.is_available:
logger.info(
"[封面生成] 步骤E1-从渲染视频抽帧: plan_id=%s url=%s",
plan_id,
primary_video_url[:80],
)
snapshots = mk_client.extract_frames(
video_url=primary_video_url,
strategy="SpecifiedFrames",
max_frames=1,
poll_interval=2.0,
max_poll_attempts=5,
max_retries=0,
)
if snapshots:
raw = snapshots[0].get("image_url") or snapshots[0].get("url") or ""
if raw:
cover_url_from_task = _persist_cover_frame(raw, plan_id)
logger.info(
"[封面生成] 统一管道封面(步骤E1-rendered-video): plan_id=%s url=%s",
plan_id,
cover_url_from_task[:80],
)
except Exception:
logger.warning(
"[封面生成] 步骤E1从渲染视频抽帧失败: plan_id=%s",
plan_id,
exc_info=True,
)
# 步骤 E2:当 A/B/C/D/E1 均未命中(如历史预览任务无 cover_url)时,
# 直接从用户选择的第一个视频素材中抽取封面帧作为兜底。API 请求内短超时,不阻塞。
if not cover_url_from_task and body.asset_ids:
from packages.adapters.sqlalchemy_impl.asset_repository import (
SQLAlchemyAssetRepository,
)
from packages.shared.mediakit_client import get_mediakit_client
from packages.shared.storage import get_shared_storage_service
asset_repo = SQLAlchemyAssetRepository(db)
storage_svc = get_shared_storage_service()
mk_client = get_mediakit_client()
# 从 plan.config 读取完整标题样式,E2 从源素材抽帧时叠加(源素材本身无标题)
_e2_title_cfg = (plan.config or {}).get("title", {}) or {}
if not isinstance(_e2_title_cfg, dict):
_e2_title_cfg = {}
_e2_title_text = (_e2_title_cfg.get("text", "") or "").strip() if _e2_title_cfg.get("enabled", True) else ""
# 读取标题样式:前端可能传 color 或 font_color,都兼容
_e2_title_color = _e2_title_cfg.get("color") or _e2_title_cfg.get("font_color") or "#ffffff"
_e2_title_position = _e2_title_cfg.get("position", "bottom") or "bottom"
_e2_title_font_size = _e2_title_cfg.get("font_size") or _e2_title_cfg.get("size")
if mk_client.is_available:
for aid in body.asset_ids:
try:
asset = asset_repo.get(aid)
if not asset or asset.file_type != "video":
continue
sk = asset.storage_key or ""
if not sk:
continue
src_url = sk if sk.startswith("http") else storage_svc.get_url(sk)
if not src_url:
continue
logger.info(
"[封面生成] 步骤E-从素材抽帧: plan_id=%s asset_id=%s url=%s",
plan_id,
aid,
src_url[:80],
)
snapshots = mk_client.extract_frames(
video_url=src_url,
strategy="SpecifiedFrames",
max_frames=1,
poll_interval=2.0,
max_poll_attempts=5,
max_retries=0,
)
if snapshots:
raw = snapshots[0].get("image_url") or snapshots[0].get("url") or ""
if raw:
cover_url_from_task = _persist_cover_frame(
raw,
plan_id,
title_text=_e2_title_text,
title_color=_e2_title_color,
title_position=_e2_title_position,
title_font_size=_e2_title_font_size,
)
logger.info(
"[封面生成] 统一管道封面(步骤E-source-asset): plan_id=%s url=%s",
plan_id,
cover_url_from_task[:80],
)
break
except Exception:
logger.warning(
"[封面生成] 步骤E从素材抽帧失败: plan_id=%s asset_id=%s",
plan_id,
aid,
exc_info=True,
)
if cover_url_from_task: if cover_url_from_task:
# 标题已在预览视频渲染时烧录(ASS字幕),封面帧自然包含标题 # 标题已在预览视频渲染时烧录(ASS字幕),封面帧自然包含标题
cover_data = { cover_data = {
@@ -325,13 +510,13 @@ def generate_cover(
return GenerateCoverResponse(plan_id=plan_id, cover=cover_data) return GenerateCoverResponse(plan_id=plan_id, cover=cover_data)
logger.warning( logger.warning(
"[封面生成] 统一管道未找到 cover_url: plan_id=%s", "[封面生成] 统一管道未找到 cover_url (A/B/C/D均未命中): plan_id=%s",
plan_id, plan_id,
) )
# ai_frame/ai_regenerate 类型必须从渲染管道获取,不再回退到 AI 服务 # ai_frame/ai_regenerate 类型必须从渲染管道获取,不再回退到 AI 服务
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="封面尚未生成,请先重新生成预览视频以触发封面自动提取", detail="封面生成失败:未找到可抽帧的视频素材,请确认已上传视频素材后重试",
) )
from packages.shared.ai_service import run_generate_cover from packages.shared.ai_service import run_generate_cover
+32 -15
View File
@@ -5,7 +5,6 @@
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
from app.auth import AuthenticatedUser, get_current_user from app.auth import AuthenticatedUser, get_current_user
@@ -265,22 +264,38 @@ def create_preview_generation_task(
if not video_ratio and request.template_id: if not video_ratio and request.template_id:
video_ratio = _infer_video_ratio_from_template(request.template_id, db, user_id) video_ratio = _infer_video_ratio_from_template(request.template_id, db, user_id)
# 根据 video_ratio 计算输出分辨率(默认竖屏 1080x1920)
output_width, output_height = 1080, 1920
if video_ratio:
parts = video_ratio.split(":")
if len(parts) == 2:
try:
w, h = int(parts[0]), int(parts[1])
base = 1920
if w < h:
# 竖屏
output_width = round(base * w / h)
output_height = base
else:
# 横屏
output_width = base
output_height = round(base * h / w)
# 对齐到偶数
output_width = output_width - output_width % 2
output_height = output_height - output_height % 2
except (ValueError, ZeroDivisionError):
output_width, output_height = 1080, 1920
resolution = f"{output_width}x{output_height}"
logger.info(
"[预览生成] 分辨率: video_ratio=%s%s (%dx%d)",
video_ratio, resolution, output_width, output_height,
)
# 从模板读取 editing_mode / mode 作为 strategy_id(渲染 pipeline 的 mode 参数) # 从模板读取 editing_mode / mode 作为 strategy_id(渲染 pipeline 的 mode 参数)
strategy_id = _resolve_strategy_id_from_template(request.template_id, db, user_id) strategy_id = _resolve_strategy_id_from_template(request.template_id, db, user_id)
# 处理标题配置:如果有标题文本,序列化到 custom_title 字段传递给 worker
title_config = request.title_config or {} 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) use_case = CreateGenerationTaskUseCase(generation_task_repository)
@@ -300,12 +315,14 @@ def create_preview_generation_task(
asset_select_mode="", asset_select_mode="",
batch_id="", batch_id="",
video_title=request.video_title, video_title=request.video_title,
resolution="", resolution=resolution,
bgm_config=request.bgm_config or {}, bgm_config=request.bgm_config or {},
auto_retry_enabled=False, auto_retry_enabled=False,
auto_retry_max=0, auto_retry_max=0,
is_preview=True, is_preview=True,
custom_title=custom_title_value, title_config=title_config,
output_width=output_width,
output_height=output_height,
) )
) )
except ValueError as e: except ValueError as e:
+197 -5
View File
@@ -16,6 +16,7 @@ from app.core.task_enqueue import (
from app.dependencies import ( from app.dependencies import (
get_asset_library_repository, get_asset_library_repository,
get_asset_repository, get_asset_repository,
get_db_session,
get_generated_video_repository, get_generated_video_repository,
get_generation_task_repository, get_generation_task_repository,
get_project_repository, get_project_repository,
@@ -32,6 +33,7 @@ from app.schemas.generation_task import (
ListGenerationTasksResponse, ListGenerationTasksResponse,
) )
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from packages.application import ( from packages.application import (
CreateGenerationTaskCommand, CreateGenerationTaskCommand,
@@ -68,7 +70,7 @@ def _to_generation_task_response(task) -> GenerationTaskResponse:
output_width=getattr(task, "output_width", 1280), output_width=getattr(task, "output_width", 1280),
output_height=getattr(task, "output_height", 720), output_height=getattr(task, "output_height", 720),
cover_url=getattr(task, "cover_url", ""), cover_url=getattr(task, "cover_url", ""),
custom_title=getattr(task, "custom_title", ""), title_config=getattr(task, "title_config", {}) or {},
logs=getattr(task, "logs", "[]"), logs=getattr(task, "logs", "[]"),
status=task.status, status=task.status,
progress=task.progress, progress=task.progress,
@@ -142,6 +144,54 @@ def _select_assets_from_library(
return [a.id for a in ready_video_assets] return [a.id for a in ready_video_assets]
def _writeback_edit_plan_config(
plan_id: str,
task_id: str,
title_config: dict | None,
db: Session,
) -> None:
"""任务入队成功后,回写 EditPlan.configgeneration_task_id + title_config。
用 merge 方式更新,不整体覆盖 config,避免丢失其他字段。
失败只记日志,不影响任务创建。
"""
if not plan_id:
return
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
plan_model = db.query(EditPlanModel).filter(EditPlanModel.id == plan_id).first()
if plan_model is None:
logger.warning("[生成任务] 回写plan.config失败: plan不存在 plan_id=%s", plan_id)
return
current_config = plan_model.config if isinstance(plan_model.config, dict) else {}
merged = dict(current_config)
merged["generation_task_id"] = task_id
if title_config:
merged["title_config"] = title_config
plan_model.config = merged
db.commit()
logger.info(
"[生成任务] 回写plan.config成功: plan_id=%s task_id=%s keys=%s",
plan_id,
task_id,
list(merged.keys()),
)
except Exception as e:
logger.warning(
"[生成任务] 回写plan.config异常(不影响任务创建): plan_id=%s error=%s",
plan_id,
e,
exc_info=True,
)
try:
db.rollback()
except Exception:
pass
def _resolve_project_and_library( def _resolve_project_and_library(
request: CreateGenerationTaskRequest, request: CreateGenerationTaskRequest,
project_repository: Any, project_repository: Any,
@@ -187,6 +237,7 @@ def create_generation_task(
project_repository: Any = Depends(get_project_repository), project_repository: Any = Depends(get_project_repository),
asset_library_repository: Any = Depends(get_asset_library_repository), asset_library_repository: Any = Depends(get_asset_library_repository),
asset_repository: Any = Depends(get_asset_repository), asset_repository: Any = Depends(get_asset_repository),
db: Session = Depends(get_db_session),
) -> BatchGenerationTaskResponse: ) -> BatchGenerationTaskResponse:
logger.info( logger.info(
"[生成任务] 接收请求: user_id=%s, template_id=%s, asset_count=%d, mode=%s, count=%d", "[生成任务] 接收请求: user_id=%s, template_id=%s, asset_count=%d, mode=%s, count=%d",
@@ -242,6 +293,89 @@ def create_generation_task(
detail="当前项目没有符合条件的视频素材,请先上传并等待导入完成后再生成。", detail="当前项目没有符合条件的视频素材,请先上传并等待导入完成后再生成。",
) )
# ── 兜底复用预览产物 ──
# 前端刷新后 previewTaskId 丢失,降级调 create 接口时,
# 如果同一 edit_plan 有已完成的预览任务,直接复用(秒出)。
if request.source_edit_plan_id and not request.is_preview:
try:
from packages.adapters.sqlalchemy_impl.models import (
GenerationTaskModel,
)
_preview_model = (
db.query(GenerationTaskModel)
.filter(
GenerationTaskModel.source_edit_plan_id == request.source_edit_plan_id,
GenerationTaskModel.is_preview.is_(True),
GenerationTaskModel.status == "completed",
GenerationTaskModel.created_by_user_id == authenticated_user.user.id,
)
.order_by(GenerationTaskModel.created_at.desc())
.first()
)
if _preview_model is not None:
# 校验分辨率一致性(与 confirm 端点逻辑相同)
req_w = request.output_width or 0
req_h = request.output_height or 0
src_w = getattr(_preview_model, "output_width", 0) or 0
src_h = getattr(_preview_model, "output_height", 0) or 0
resolution_match = (req_w == 0 or req_w == src_w) and (req_h == 0 or req_h == src_h)
if resolution_match:
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
_to_domain,
)
preview_task = _to_domain(_preview_model)
# 如果传了标题,更新 title_config
fallback_title_config = None
if request.title_config and request.title_config.get("text", "").strip():
fallback_title_config = dict(preview_task.title_config or {})
fallback_title_config.update(request.title_config)
preview_task.mark_confirmed(
cover_url=request.cover_url or preview_task.cover_url,
output_width=request.output_width or preview_task.output_width,
output_height=request.output_height or preview_task.output_height,
title_config=fallback_title_config,
)
generation_task_repository.update(preview_task)
# 同步标题到 EditPlan.config
if fallback_title_config:
_writeback_edit_plan_config(
plan_id=request.source_edit_plan_id,
task_id=preview_task.id,
title_config=fallback_title_config,
db=db,
)
logger.info(
"[生成任务] 兜底复用预览产物: preview_task_id=%s, plan_id=%s",
preview_task.id,
request.source_edit_plan_id,
)
return BatchGenerationTaskResponse(
items=[_to_generation_task_response(preview_task)],
total=1,
)
else:
logger.info(
"[生成任务] 兜底复用跳过(分辨率不一致): plan_id=%s, src=%sx%s, req=%sx%s",
request.source_edit_plan_id,
src_w,
src_h,
req_w,
req_h,
)
except Exception:
logger.warning(
"[生成任务] 兜底复用预览产物异常(不影响主流程): plan_id=%s",
request.source_edit_plan_id,
exc_info=True,
)
use_case = CreateGenerationTaskUseCase(generation_task_repository) use_case = CreateGenerationTaskUseCase(generation_task_repository)
count = request.count count = request.count
created_tasks = [] created_tasks = []
@@ -303,10 +437,53 @@ def create_generation_task(
output_width=request.output_width, output_width=request.output_width,
output_height=request.output_height, output_height=request.output_height,
cover_url=request.cover_url, cover_url=request.cover_url,
custom_title=request.custom_title, title_config=request.title_config or {},
) )
) )
try: try:
# 兜底关联编辑计划:前端未传 source_edit_plan_id 时,
# 通过 template_id + user_id 在 DB 层直接查找最新的 plan。
# 必须在 enqueue 之前执行,避免 worker 读取时 source_edit_plan_id 为空(竞态条件)
if not task.source_edit_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_plan_model = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id,
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _plan_model:
task.source_edit_plan_id = _plan_model.id
generation_task_repository.update(task)
logger.info(
"[生成任务] 自动关联编辑计划: task_id=%s plan_id=%s",
task.id,
_plan_model.id,
)
except Exception:
logger.warning(
"[生成任务] 查找关联编辑计划失败(不影响主流程): task_id=%s",
task.id,
exc_info=True,
)
# 回写 plan.config:必须在 enqueue 之前执行,
# 确保 worker 读取 plan 时 config 中已包含 generation_task_id。
# 只在首个任务时回写一次,避免批量生成时循环覆盖。
_effective_plan_id = task.source_edit_plan_id
if _effective_plan_id and len(created_tasks) == 0:
_writeback_edit_plan_config(
plan_id=_effective_plan_id,
task_id=task.id,
title_config=request.title_config,
db=db,
)
if safe_enqueue_generation_task( if safe_enqueue_generation_task(
task, task,
generation_task_repository, generation_task_repository,
@@ -351,6 +528,7 @@ def confirm_generation(
authenticated_user: AuthenticatedUser = Depends(get_current_user), authenticated_user: AuthenticatedUser = Depends(get_current_user),
generation_task_repository: Any = Depends(get_generation_task_repository), generation_task_repository: Any = Depends(get_generation_task_repository),
project_repository: Any = Depends(get_project_repository), project_repository: Any = Depends(get_project_repository),
db: Session = Depends(get_db_session),
) -> BatchGenerationTaskResponse: ) -> BatchGenerationTaskResponse:
"""确认生成 -- 复用预览渲染产物(预览与正式品质一致)。 """确认生成 -- 复用预览渲染产物(预览与正式品质一致)。
@@ -379,13 +557,29 @@ def confirm_generation(
resolution_match = (req_w == 0 or req_w == src_w) and (req_h == 0 or req_h == src_h) resolution_match = (req_w == 0 or req_w == src_w) and (req_h == 0 or req_h == src_h)
if resolution_match: if resolution_match:
# 如果用户传了 custom_title,同步更新 title_config
confirmed_title_config = None
if request.custom_title and request.custom_title.strip():
confirmed_title_config = dict(getattr(source_task, "title_config", {}) or {})
confirmed_title_config["text"] = request.custom_title.strip()
source_task.mark_confirmed( source_task.mark_confirmed(
cover_url=request.cover_url, cover_url=request.cover_url,
custom_title=request.custom_title,
output_width=request.output_width, output_width=request.output_width,
output_height=request.output_height, output_height=request.output_height,
title_config=confirmed_title_config,
) )
generation_task_repository.update(source_task) generation_task_repository.update(source_task)
# 同步标题到 EditPlan.config
if confirmed_title_config and source_task.source_edit_plan_id:
_writeback_edit_plan_config(
plan_id=source_task.source_edit_plan_id,
task_id=source_task.id,
title_config=confirmed_title_config,
db=db,
)
logger.info( logger.info(
"[确认生成] 复用预览产物: task_id=%s, user_id=%s", "[确认生成] 复用预览产物: task_id=%s, user_id=%s",
task_id, task_id,
@@ -427,7 +621,6 @@ def confirm_generation(
output_width=request.output_width, output_width=request.output_width,
output_height=request.output_height, output_height=request.output_height,
cover_url=request.cover_url, cover_url=request.cover_url,
custom_title=request.custom_title,
) )
) )
@@ -560,7 +753,6 @@ def retry_generation_task(
output_width=getattr(task, "output_width", 1280), output_width=getattr(task, "output_width", 1280),
output_height=getattr(task, "output_height", 720), output_height=getattr(task, "output_height", 720),
cover_url=getattr(task, "cover_url", ""), cover_url=getattr(task, "cover_url", ""),
custom_title=getattr(task, "custom_title", ""),
) )
) )
try: try:
@@ -1,10 +1,9 @@
"""模板编辑器 API 路由包. """模板编辑器 API 路由包.
将原来 2560 行的 templates_editor.py 巨无霸拆分为 12 个模块: 模块拆分
- schemas.py: 所有 Pydantic model - schemas.py: 所有 Pydantic model
- dependencies.py: 依赖注入 - dependencies.py: 依赖注入
- _utils.py: 工具函数 - _utils.py: 工具函数
- _fallback.py: 自动兜底逻辑
- draft.py: 草稿管理(详情/更新/发布/版本/回滚) - draft.py: 草稿管理(详情/更新/发布/版本/回滚)
- clips.py: 片段管理(CRUD/分割/合并/重排/批量删除/从素材创建) - clips.py: 片段管理(CRUD/分割/合并/重排/批量删除/从素材创建)
- adjustments.py: 片段调整(速度/音量/裁剪/批量调速) - adjustments.py: 片段调整(速度/音量/裁剪/批量调速)
@@ -13,7 +12,6 @@
- export.py: 导出配置 - export.py: 导出配置
- subtitles.py: 字幕管理 - subtitles.py: 字幕管理
- ai_features.py: AI 推荐 - ai_features.py: AI 推荐
- generation.py: 生成(触发/进度/记录)
- timeline.py: 时间线 - timeline.py: 时间线
挂载路径: /api/v1/templates/{template_id}/editor/ 挂载路径: /api/v1/templates/{template_id}/editor/
@@ -34,7 +32,6 @@ from .dependencies import get_draft_plan_id, get_editor_services # noqa: F401
from .draft import router as draft_router from .draft import router as draft_router
from .effects import router as effects_router from .effects import router as effects_router
from .export import router as export_router from .export import router as export_router
from .generation import router as generation_router
from .subtitles import router as subtitles_router from .subtitles import router as subtitles_router
from .timeline import router as timeline_router from .timeline import router as timeline_router
@@ -51,7 +48,6 @@ _sub_routers = [
export_router, export_router,
subtitles_router, subtitles_router,
ai_features_router, ai_features_router,
generation_router,
timeline_router, timeline_router,
] ]
@@ -1,227 +0,0 @@
"""模板编辑器自动兜底逻辑.
generate_editor_draft 触发生成前的自动修复流程:
1. draft → editing 状态迁移
2. 无片段时从模板复制片段配置
3. 为无素材片段分配指定素材
4. 项目有素材库时自动选素材
"""
from __future__ import annotations
import logging
import random
from typing import Any
from app.services.edit_plan_service import EditPlanService
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.template_clip_config_repository import (
SQLAlchemyTemplateClipConfigRepository,
)
from packages.adapters.sqlalchemy_impl.template_repository import (
SQLAlchemyTemplateRepository,
)
from packages.domain.edit_plan import EditPlanStatus
logger = logging.getLogger(__name__)
def _auto_fallback_draft_to_editing(svc: EditPlanService, plan_id: str, plan_check) -> None:
"""自动兜底 1: draft → editing"""
if plan_check.status == EditPlanStatus.DRAFT:
logger.info("模板编辑器自动兜底: plan=%s draft→editing", plan_id)
svc.transition_status(plan_id, EditPlanStatus.EDITING)
def _auto_fallback_copy_template_clips(svc: EditPlanService, plan_id: str, plan_check, db: Session) -> None:
"""自动兜底 2: 无片段 + 有 template_id → 从模板复制片段配置"""
existing_clips = svc.count_clips(plan_id)
if existing_clips == 0 and plan_check.template_id:
logger.info(
"模板编辑器自动兜底: plan=%s 无片段,从模板 %s 复制片段配置",
plan_id,
plan_check.template_id,
)
clip_config_repo = SQLAlchemyTemplateClipConfigRepository(db)
configs = clip_config_repo.list_by_template(plan_check.template_id)
if configs:
for cfg in configs:
svc.create_clip(
plan_id=plan_id,
clip_type=cfg.clip_type.value if hasattr(cfg.clip_type, "value") else cfg.clip_type,
order=cfg.order,
template_clip_config_id=cfg.id,
duration=cfg.default_duration,
transition_effect=(
cfg.transition_effect.value
if hasattr(cfg.transition_effect, "value")
else cfg.transition_effect
),
)
logger.info(
"模板编辑器自动兜底: plan=%s 从 template_clip_configs 复制了 %d 个片段",
plan_id,
len(configs),
)
else:
tpl_repo = SQLAlchemyTemplateRepository(db)
segments = tpl_repo.list_segments(plan_check.template_id)
for seg in segments:
avg_duration = (seg.duration_min + seg.duration_max) / 2
svc.create_clip(
plan_id=plan_id,
clip_type="main",
order=seg.segment_order,
duration=avg_duration,
config={
"material_type": seg.material_type or "",
"template_segment_id": seg.id,
},
)
logger.info(
"模板编辑器自动兜底: plan=%s 从旧模板 segments 复制了 %d 个片段",
plan_id,
len(segments),
)
def _auto_fallback_assign_assets(svc: EditPlanService, plan_id: str, plan_check) -> list:
"""自动兜底 3: 为没有素材的片段分配素材。返回剩余无素材片段列表。"""
all_clips = svc.list_clips(plan_id)
clips_without_asset = [c for c in all_clips if not c.asset_id]
config_asset_ids = (plan_check.config or {}).get("asset_ids", [])
logger.info(
"模板编辑器自动兜底3 诊断: plan=%s total_clips=%d " "clips_without_asset=%d config_asset_ids=%r",
plan_id,
len(all_clips),
len(clips_without_asset),
config_asset_ids[:5] if config_asset_ids else [],
)
if clips_without_asset and config_asset_ids:
logger.info(
"模板编辑器自动兜底3: plan=%s%d 个无素材片段分配 %d 个指定素材",
plan_id,
len(clips_without_asset),
len(config_asset_ids),
)
assigned = 0
for i, clip in enumerate(clips_without_asset):
asset_idx = i % len(config_asset_ids)
try:
svc.assign_asset(clip.id, config_asset_ids[asset_idx])
assigned += 1
except Exception as exc:
logger.error(
"模板编辑器自动兜底3: plan=%s clip=%s 分配素材 %s 失败: %s",
plan_id,
clip.id,
config_asset_ids[asset_idx],
exc,
)
logger.info(
"模板编辑器自动兜底3: plan=%s 素材分配完成 assigned=%d/%d",
plan_id,
assigned,
len(clips_without_asset),
)
# 重新检查剩余无素材片段
all_clips_after = svc.list_clips(plan_id)
clips_without_asset = [c for c in all_clips_after if not c.asset_id]
if clips_without_asset:
logger.warning(
"模板编辑器自动兜底3: plan=%s 仍有 %d 个片段无素材",
plan_id,
len(clips_without_asset),
)
elif not clips_without_asset:
logger.info("模板编辑器自动兜底3: plan=%s 所有片段已有素材,跳过", plan_id)
elif not config_asset_ids:
logger.info(
"模板编辑器自动兜底3: plan=%s config.asset_ids 为空,跳过分配",
plan_id,
)
return clips_without_asset
def _auto_fallback_auto_material_mode(
svc: EditPlanService,
plan_id: str,
plan_check,
clips_without_asset: list,
asset_library_repo: Any,
asset_repo: Any,
user_id: str = "",
) -> None:
"""自动兜底 4: 自动选素材分配给无素材片段
查找策略(按优先级):
1. plan 有 project_id → 从项目素材库查找
2. plan 无 project_id 但有 user_id → 从用户上传的素材中查找
"""
if not clips_without_asset:
return
ready_videos: list = []
source_desc = ""
# 策略 1: 通过 project_id 查找项目素材库
if plan_check.project_id:
libs = asset_library_repo.find_by_project(plan_check.project_id)
video_lib = None
for lib in libs:
lib_kind = lib.kind.value if hasattr(lib.kind, "value") else lib.kind
if lib_kind == "video":
video_lib = lib
break
if video_lib:
assets = asset_repo.find_by_library(video_lib.id)
ready_videos = [
a
for a in assets
if (a.status.value if hasattr(a.status, "value") else a.status) == "ready"
and a.mime_type
and a.mime_type.startswith("video")
]
source_desc = f"素材库 {video_lib.name}"
# 策略 2: 通过 user_id 查找用户上传的素材
if not ready_videos and user_id and hasattr(asset_repo, "find_ready_videos_by_user"):
logger.info(
"模板编辑器自动兜底4: plan=%s project_id 为空,尝试通过 user_id=%s 查找素材",
plan_id,
user_id,
)
ready_videos = asset_repo.find_ready_videos_by_user(user_id)
source_desc = f"用户上传 (user_id={user_id[:8]}...)"
if not ready_videos:
logger.warning(
"模板编辑器自动兜底4: plan=%s 未找到可用素材 (project_id=%s, user_id=%s)",
plan_id,
plan_check.project_id or "(empty)",
user_id[:8] + "..." if user_id else "(empty)",
)
return
logger.info(
"模板编辑器自动兜底4: plan=%s 自动选素材分配给 %d 个无素材片段 (来源: %s, 共 %d 个)",
plan_id,
len(clips_without_asset),
source_desc,
len(ready_videos),
)
random.shuffle(ready_videos)
for i, clip in enumerate(clips_without_asset):
asset = ready_videos[i % len(ready_videos)]
svc.assign_asset(clip.id, asset.id)
logger.info(
"模板编辑器自动兜底4: plan=%s%s 分配了 %d 个素材给 %d 个片段",
plan_id,
source_desc,
len(ready_videos),
len(clips_without_asset),
)
@@ -3,7 +3,6 @@
核心依赖: 核心依赖:
- get_editor_services: 获取模板+计划服务 - get_editor_services: 获取模板+计划服务
- get_draft_plan_id: 根据 template_id 获取或创建草稿,返回 plan_id - get_draft_plan_id: 根据 template_id 获取或创建草稿,返回 plan_id
- _check_queue_limits: 生成队列限流检查
""" """
from __future__ import annotations from __future__ import annotations
@@ -11,7 +10,6 @@ from __future__ import annotations
import logging import logging
from app.auth import AuthenticatedUser, get_current_user from app.auth import AuthenticatedUser, get_current_user
from app.core.task_enqueue import GLOBAL_PENDING_LIMIT, USER_PENDING_LIMIT
from app.dependencies import get_db_session from app.dependencies import get_db_session
from app.services.edit_plan_service import EditPlanService from app.services.edit_plan_service import EditPlanService
from app.services.edit_template_service import EditTemplateService from app.services.edit_template_service import EditTemplateService
@@ -113,29 +111,3 @@ def get_draft_plan_id(
user_id, user_id,
) )
return plan.id return plan.id
def _check_queue_limits(gen_task_repo, user_id: str) -> None:
"""队列限流预检查"""
try:
has_count = (
hasattr(gen_task_repo, "count_pending_by_user")
and hasattr(gen_task_repo, "count_pending_total")
)
if has_count:
user_pending = gen_task_repo.count_pending_by_user(user_id)
global_pending = gen_task_repo.count_pending_total()
if user_pending >= USER_PENDING_LIMIT:
raise HTTPException(
status_code=429,
detail=f"您的待处理任务过多(当前 {user_pending}/{USER_PENDING_LIMIT}),请等待完成后再提交",
)
if global_pending >= GLOBAL_PENDING_LIMIT:
raise HTTPException(
status_code=503,
detail="系统繁忙,请稍后再试",
)
except HTTPException:
raise
except Exception as e:
logger.warning("[模板编辑器队列限流] 检查失败,跳过: %s", e)
@@ -17,6 +17,8 @@ from fastapi import APIRouter, Depends, HTTPException, Query, status
from .dependencies import get_draft_plan_id, get_editor_services from .dependencies import get_draft_plan_id, get_editor_services
from .schemas import ( from .schemas import (
EditorClipBatchUpdateRequest,
EditorClipBatchUpdateResponse,
EditorDraftResponse, EditorDraftResponse,
EditorPublishResponse, EditorPublishResponse,
EditorRollbackRequest, EditorRollbackRequest,
@@ -126,11 +128,7 @@ def list_template_versions(
clip_count=len(v.clip_configs), clip_count=len(v.clip_configs),
change_note=v.change_note, change_note=v.change_note,
published_by=v.published_by, published_by=v.published_by,
created_at=( created_at=(v.created_at.isoformat() if hasattr(v.created_at, "isoformat") else str(v.created_at)),
v.created_at.isoformat()
if hasattr(v.created_at, "isoformat")
else str(v.created_at)
),
) )
for v in versions for v in versions
] ]
@@ -162,3 +160,35 @@ def rollback_template(
new_version=tpl.version, new_version=tpl.version,
clip_count=len(clip_configs), clip_count=len(clip_configs),
) )
@router.put("/clips", response_model=EditorClipBatchUpdateResponse)
def batch_update_clips(
template_id: str,
req: EditorClipBatchUpdateRequest,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
_: AuthenticatedUser = Depends(get_current_user),
):
"""批量替换草稿clips(全量覆盖,用于前端选择素材后同步片段)
事务保证:清空→创建→标记ready 在同一数据库事务内完成,
任何步骤失败时自动回滚,避免数据不一致。
"""
_, plan_svc = services
plan_svc.get_plan_or_raise(plan_id)
clips_data = []
for clip_item in req.clips:
item = {
"asset_id": clip_item.asset_id,
"start_time": clip_item.start_time,
"duration": clip_item.duration,
}
if clip_item.order is not None:
item["order"] = clip_item.order
clips_data.append(item)
plan_svc.replace_all_clips_transactional(plan_id, clips_data)
return EditorClipBatchUpdateResponse(plan_id=plan_id, clip_count=len(req.clips))
@@ -1,383 +0,0 @@
"""草稿生成路由.
端点:
- POST /generate 触发生成
- GET /generation-status 生成进度
- GET /generations 生成记录列表
"""
from __future__ import annotations
import json
import logging
from typing import Any, Optional
from app.auth import AuthenticatedUser, get_current_user
from app.core.celery_app import celery_app
from app.core.storage import OSSStorageService, get_storage_service
from app.dependencies import (
get_asset_library_repository,
get_asset_repository,
get_db_session,
get_generated_video_repository,
)
from app.schemas.generation_task import GenerationTaskResponse
from app.services.edit_plan_service import EditPlanService
from app.services.edit_template_service import EditTemplateService
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
SQLAlchemyGenerationTaskRepository,
)
from packages.application.generated_videos import ListGeneratedVideosByTaskUseCase
from packages.application.generation_tasks import (
CreateGenerationTaskCommand,
CreateGenerationTaskUseCase,
)
from packages.domain.edit_plan import EditPlanStatus
from ._fallback import (
_auto_fallback_assign_assets,
_auto_fallback_auto_material_mode,
_auto_fallback_copy_template_clips,
_auto_fallback_draft_to_editing,
)
from .dependencies import _check_queue_limits, get_draft_plan_id, get_editor_services
from .schemas import (
ClipStatusItem,
EditPlanGenerateRequest,
EditPlanGenerateResponse,
EditPlanGenerationsResponse,
EditPlanGenerationStatusResponse,
)
logger = logging.getLogger(__name__)
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),
current_user: AuthenticatedUser = Depends(get_current_user),
asset_library_repo: Any = Depends(get_asset_library_repository),
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)
# 自动兜底流程
_auto_fallback_draft_to_editing(plan_svc, plan_id, plan_check)
_auto_fallback_copy_template_clips(plan_svc, plan_id, plan_check, db)
clips_without_asset = _auto_fallback_assign_assets(plan_svc, plan_id, plan_check)
_auto_fallback_auto_material_mode(
plan_svc,
plan_id,
plan_check,
clips_without_asset,
asset_library_repo,
asset_repo,
user_id=str(current_user.user.id),
)
# 检查是否可复用已完成的预览产物(预览品质已与正式一致)
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()
gen_task_repo.update(reusable_task)
# 将产物 URL 写入 plan config
rendered_url = _get_task_output_url(reusable_task, gen_task_repo, db)
plan_svc.update_plan_config(
plan_id,
{
"generation_task_id": reusable_task.id,
"rendered_storage_key": rendered_url, # 统一用 rendered_storage_key
},
)
plan_svc.transition_status(plan_id, EditPlanStatus.COMPLETED)
updated_plan = plan_svc.get_plan_or_raise(plan_id)
logger.info(
"模板编辑器复用预览产物: template_id=%s plan_id=%s task_id=%s by user=%s",
template_id,
plan_id,
reusable_task.id,
current_user.user.id,
)
return EditPlanGenerateResponse(
plan_id=plan_id,
plan_status=updated_plan.status.value if hasattr(updated_plan.status, "value") else updated_plan.status,
generation_task_id=reusable_task.id,
clip_count=len((plan_check.config or {}).get("clips", [])),
)
# 检查是否可生成(含最后防线自动修复 + 诊断日志)
try:
can_gen, reason = plan_svc.can_generate(plan_id)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
if not can_gen:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=reason)
try:
clip_count = plan_svc.mark_clips_ready(plan_id)
user_id = current_user.user.id
_check_queue_limits(gen_task_repo, user_id)
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 "",
template_id=plan.template_id,
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,
),
)
plan_svc.update_plan_config(plan_id, {"generation_task_id": gen_task.id})
plan_svc.transition_status(plan_id, EditPlanStatus.RENDERING)
celery_app.send_task("worker.render_edit_plan", args=[plan_id])
updated_plan = plan_svc.get_plan_or_raise(plan_id)
logger.info(
"模板编辑器触发生成: template_id=%s plan_id=%s gen_task_id=%s clips=%d by user=%s",
template_id,
plan_id,
gen_task.id,
clip_count,
current_user.user.id,
)
return EditPlanGenerateResponse(
plan_id=plan_id,
plan_status=updated_plan.status.value if hasattr(updated_plan.status, "value") else updated_plan.status,
generation_task_id=gen_task.id,
clip_count=clip_count,
)
except HTTPException:
raise
except Exception as _e:
logger.exception(
"模板编辑器触发生成失败: template_id=%s plan_id=%s",
template_id,
plan_id,
)
try:
plan_svc.transition_status(plan_id, EditPlanStatus.FAILED)
except Exception:
pass
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="生成失败,请稍后重试",
) from _e
def _find_reusable_preview_task(gen_task_repo, plan_id: str, plan) -> "object | None":
"""查找该 plan 关联的已完成预览任务,判断是否可复用。
复用条件:
1. 存在 source_edit_plan_id == plan_id 的已完成预览任务
2. plan 在预览完成后未被修改(updated_at <= 预览完成时间)
Returns:
可复用的 GenerationTask,或 None
"""
try:
tasks = gen_task_repo.list_by_source_edit_plan(plan_id)
except Exception:
return None
for task in tasks:
if not getattr(task, "is_preview", False):
continue
if not task.is_completed:
continue
# 检查 plan 是否在预览完成后被修改
completed_at = getattr(task, "completed_at", None)
if completed_at and hasattr(plan, "updated_at"):
plan_updated = plan.updated_at
# 如果 plan.updated_at 为空,无法判断是否修改过,跳过
if plan_updated is None:
continue
# 如果 plan 在预览完成后又被修改了,不能复用
if plan_updated > completed_at:
continue
return task
return None
def _get_task_output_url(task, gen_task_repo, db) -> str:
"""获取任务的输出视频 URL。"""
try:
video_repo = get_generated_video_repository(db)
use_case = ListGeneratedVideosByTaskUseCase(video_repo)
videos = use_case.execute(task.id)
if videos:
url = getattr(videos[0], "file_url", "") or ""
# 规范化:合并路径中的双斜杠(保留协议头 ://)
if url:
import re as _re
url = _re.sub(r"(?<!:)//", "/", url)
return url
except Exception:
pass
return ""
@router.get("/generation-status", response_model=EditPlanGenerationStatusResponse)
def get_editor_generation_status(
template_id: str,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
storage_service: OSSStorageService = Depends(get_storage_service),
_: AuthenticatedUser = Depends(get_current_user),
) -> EditPlanGenerationStatusResponse:
"""查询草稿生成进度"""
_, plan_svc = services
try:
gen_status = plan_svc.get_generation_status(plan_id)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
plan = gen_status["plan"]
clips = gen_status["clips"]
clip_items = [
ClipStatusItem(
clip_id=c.id,
clip_type=c.clip_type,
order=c.order,
status=c.status.value if hasattr(c.status, "value") else c.status,
asset_id=c.asset_id or "",
text_content=c.text_content or "",
duration=c.duration,
)
for c in clips
]
raw_video_url = (plan.config or {}).get("rendered_storage_key", "") or (plan.config or {}).get("rendered_url", "")
video_url = ""
if raw_video_url:
if raw_video_url.startswith("http"):
video_url = raw_video_url # 已经是完整 URL
else:
try:
video_url = storage_service.get_url(raw_video_url) # storage_key -> 完整 URL
except Exception as e:
logger.warning("生成视频URL获取失败: template_id=%s error=%s", template_id, e)
video_url = raw_video_url
progress = gen_status.get("progress", 0.0)
error_message = gen_status.get("error_message", "")
gen_task_status = gen_status.get("generation_task_status")
plan_status_val = plan.status.value if hasattr(plan.status, "value") else plan.status
if plan_status_val == "completed" and progress < 100:
progress = 100.0
return EditPlanGenerationStatusResponse(
plan_id=plan_id,
plan_status=plan_status_val,
generation_task_id=gen_status["generation_task_id"],
generation_task_status=gen_task_status,
progress=progress,
video_url=video_url,
error_message=error_message,
clips=clip_items,
)
@router.get("/generations", response_model=EditPlanGenerationsResponse)
def list_editor_generations(
template_id: str,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
db: Session = Depends(get_db_session),
_: AuthenticatedUser = Depends(get_current_user),
) -> EditPlanGenerationsResponse:
"""查询草稿关联的生成记录列表"""
_, plan_svc = services
plan_svc.get_plan_or_raise(plan_id)
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
tasks = gen_task_repo.list_by_source_edit_plan(plan_id)
items = [
GenerationTaskResponse(
id=t.id,
project_id=t.project_id,
asset_library_id=t.asset_library_id,
strategy_id=t.strategy_id,
voice_library_id=t.voice_library_id,
template_id=t.template_id,
asset_ids=t.asset_ids,
title_ids=t.title_ids,
voice_ids=t.voice_ids,
source_edit_plan_id=t.source_edit_plan_id or "",
status=t.status.value if hasattr(t.status, "value") else t.status,
progress=t.progress,
result_count=t.result_count,
error_message=t.error_message,
)
for t in tasks
]
return EditPlanGenerationsResponse(items=items, total=len(items))
@@ -6,9 +6,8 @@
from __future__ import annotations from __future__ import annotations
import re as _re import re as _re
from typing import Any, Dict, List, Optional from typing import Any, List, Optional
from app.schemas.generation_task import GenerationTaskResponse
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
_EXPORT_RESOLUTION_PATTERN = _re.compile(r"^\d+x\d+$") _EXPORT_RESOLUTION_PATTERN = _re.compile(r"^\d+x\d+$")
@@ -16,58 +15,6 @@ _EXPORT_VALID_QUALITY_PRESETS = {"ultra_fast", "fast", "balanced", "high", "best
_EXPORT_VALID_FORMATS = {"mp4", "mov"} _EXPORT_VALID_FORMATS = {"mp4", "mov"}
# ── 生成状态相关 ────────────────────────────────────────────────────────────
class ClipStatusItem(BaseModel):
"""片段生成状态"""
clip_id: str
clip_type: str
order: int
status: str
asset_id: str
text_content: str
duration: float
class EditPlanGenerationStatusResponse(BaseModel):
"""剪辑计划生成进度响应体"""
plan_id: str
plan_status: str
generation_task_id: Optional[str] = None
generation_task_status: Optional[str] = None
progress: float = 0.0
video_url: str = ""
error_message: str = ""
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):
"""剪辑计划触发生成响应体"""
plan_id: str
plan_status: str
generation_task_id: str
clip_count: int
class EditPlanGenerationsResponse(BaseModel):
"""剪辑计划关联的生成记录列表响应体"""
items: List[GenerationTaskResponse]
total: int
# ── AI 推荐 ──────────────────────────────────────────────────────────────── # ── AI 推荐 ────────────────────────────────────────────────────────────────
@@ -75,12 +22,8 @@ class AIRecommendRequest(BaseModel):
"""AI 推荐片段方案请求体""" """AI 推荐片段方案请求体"""
asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表") asset_ids: List[str] = Field(default_factory=list, description="素材 ID 列表")
editing_mode: str = Field( editing_mode: str = Field(default="one_take", description="剪辑模式: one_take / pip / voice_over / voice_pip")
default="one_take", description="剪辑模式: one_take / pip / voice_over / voice_pip" target_duration: float = Field(default=30.0, ge=1.0, le=600.0, description="目标时长(秒)")
)
target_duration: float = Field(
default=30.0, ge=1.0, le=600.0, description="目标时长(秒)"
)
class AIRecommendClipItem(BaseModel): class AIRecommendClipItem(BaseModel):
@@ -107,8 +50,6 @@ class AIRecommendResponse(BaseModel):
confidence: float = Field(..., ge=0.0, le=1.0, description="AI 推荐置信度 (0~1)") confidence: float = Field(..., ge=0.0, le=1.0, description="AI 推荐置信度 (0~1)")
# ── BGM ──────────────────────────────────────────────────────────────────── # ── BGM ────────────────────────────────────────────────────────────────────
@@ -224,9 +165,7 @@ class ClipBatchDeleteResponse(BaseModel):
class ClipsFromAssetsRequest(BaseModel): class ClipsFromAssetsRequest(BaseModel):
"""从素材批量创建片段请求""" """从素材批量创建片段请求"""
asset_ids: List[str] = Field( asset_ids: List[str] = Field(..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾")
..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾"
)
clip_type: str = Field(default="main", description="片段类型,默认 main") clip_type: str = Field(default="main", description="片段类型,默认 main")
@@ -501,6 +440,28 @@ class EditorClipUpdateRequest(BaseModel):
config: Optional[dict[str, Any]] = None config: Optional[dict[str, Any]] = None
class EditorClipBatchItem(BaseModel):
"""批量更新clips的单个片段"""
asset_id: str = Field(default="", max_length=100, description="关联素材ID,可为空(占位片段)")
start_time: float = Field(default=0.0, ge=0.0)
duration: float = Field(default=0.0, ge=0.0)
order: Optional[int] = Field(default=None, ge=0, description="排序,None表示按数组顺序")
class EditorClipBatchUpdateRequest(BaseModel):
"""批量替换clips请求(全量覆盖)"""
clips: List[EditorClipBatchItem] = Field(default_factory=list)
class EditorClipBatchUpdateResponse(BaseModel):
"""批量更新clips响应"""
plan_id: str
clip_count: int
class EditorPublishResponse(BaseModel): class EditorPublishResponse(BaseModel):
"""发布草稿响应""" """发布草稿响应"""
+56 -1
View File
@@ -20,6 +20,8 @@ from app.schemas.tts import (
SaveToLibraryRequest, SaveToLibraryRequest,
SaveToLibraryResponse, SaveToLibraryResponse,
TTSJobResponse, TTSJobResponse,
TTSPreviewRequest,
TTSPreviewResponse,
TTSStatusResponse, TTSStatusResponse,
TTSSynthesizeRequest, TTSSynthesizeRequest,
TTSSynthesizeResponse, TTSSynthesizeResponse,
@@ -31,7 +33,7 @@ from packages.adapters.sqlalchemy_impl.tts_job_repository import (
SQLAlchemyTTSJobRepository, SQLAlchemyTTSJobRepository,
) )
from packages.adapters.sqlalchemy_impl.voice_library_repository import SQLAlchemyVoiceLibraryRepository from packages.adapters.sqlalchemy_impl.voice_library_repository import SQLAlchemyVoiceLibraryRepository
from packages.application.cosyvoice_service import CosyVoiceService from packages.application.cosyvoice_service import CosyVoiceError, CosyVoiceService
from packages.application.tts_job.streaming_service import TTSStreamingService from packages.application.tts_job.streaming_service import TTSStreamingService
from packages.application.tts_job.use_cases import ( from packages.application.tts_job.use_cases import (
CreateTTSJobUseCase, CreateTTSJobUseCase,
@@ -373,6 +375,59 @@ def save_tts_job_to_library(
) )
@router.post("/preview", response_model=TTSPreviewResponse)
def preview_tts(
request: TTSPreviewRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
cosyvoice_service: CosyVoiceService = Depends(get_cosyvoice_service),
voice_clone_repo=Depends(get_voice_clone_profile_repository),
) -> TTSPreviewResponse:
"""TTS 预览(试听)——同步合成,立即返回音频 URL。
用于前端预览配音效果,限制文本长度 200 字以内。
支持预设音色和克隆音色:克隆音色传的是 profile UUID,需解析为 CosyVoice voice_id。
"""
# 解析 voice_id:前端可能传 VoiceCloneProfile UUID 或预设音色 ID
actual_voice_id = request.voice_id
profile = voice_clone_repo.get(request.voice_id)
if profile is not None:
# 命中克隆音色 profile — 校验归属权限
if profile.user_id != authenticated_user.user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="无权访问该音色",
)
if not profile.voice_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="音色克隆尚未完成,请稍后再试",
)
actual_voice_id = profile.voice_id
try:
result = cosyvoice_service.synthesize_speech(
text=request.text,
voice_id=actual_voice_id,
speed=request.speed,
)
except CosyVoiceError as e:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"TTS 合成失败: {e}",
) from e
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e),
) from e
return TTSPreviewResponse(
audio_url=result.audio_url,
duration=result.duration if result.duration and result.duration > 0 else None,
)
@router.websocket("/ws/tts/stream") @router.websocket("/ws/tts/stream")
async def tts_websocket_stream( async def tts_websocket_stream(
websocket: WebSocket, websocket: WebSocket,
+7 -3
View File
@@ -10,7 +10,7 @@ class ConfirmGenerationRequest(BaseModel):
output_width: int = Field(default=1080, ge=100, description="输出视频宽度") output_width: int = Field(default=1080, ge=100, description="输出视频宽度")
output_height: int = Field(default=1920, ge=100, description="输出视频高度") output_height: int = Field(default=1920, ge=100, description="输出视频高度")
cover_url: str = Field(default="", description="自定义封面图片 URL") cover_url: str = Field(default="", description="自定义封面图片 URL")
custom_title: str = Field(default="", description="自定义视频标题") custom_title: str = Field(default="", description="用户自定义标题文本,非空时同步到任务和编辑计划")
class CreateGenerationTaskRequest(BaseModel): class CreateGenerationTaskRequest(BaseModel):
@@ -33,6 +33,11 @@ class CreateGenerationTaskRequest(BaseModel):
voice_ids: list[str] = Field(default_factory=list) voice_ids: list[str] = Field(default_factory=list)
# ── 来源剪辑计划 ── # ── 来源剪辑计划 ──
source_edit_plan_id: str = "" source_edit_plan_id: str = ""
# ── 标题配置(结构化)──
title_config: dict | None = Field(
default=None,
description="标题样式对象,包含 text/font/font_size/font_color/position/bold/stroke/shadow 等。为空时不影响现有行为。",
)
# ── 视频标题 ── # ── 视频标题 ──
video_title: str = Field(default="", description="生成视频的标题/名称,为空则使用默认命名") video_title: str = Field(default="", description="生成视频的标题/名称,为空则使用默认命名")
# ── 批量生成 ── # ── 批量生成 ──
@@ -72,7 +77,6 @@ class CreateGenerationTaskRequest(BaseModel):
output_width: int = Field(default=1280, description="输出视频宽度") output_width: int = Field(default=1280, description="输出视频宽度")
output_height: int = Field(default=720, description="输出视频高度") output_height: int = Field(default=720, description="输出视频高度")
cover_url: str = Field(default="", description="封面图片 URL") cover_url: str = Field(default="", description="封面图片 URL")
custom_title: str = Field(default="", description="自定义视频标题")
@model_validator(mode="after") @model_validator(mode="after")
def _check_at_least_one_mode(self) -> "CreateGenerationTaskRequest": def _check_at_least_one_mode(self) -> "CreateGenerationTaskRequest":
@@ -108,7 +112,7 @@ class GenerationTaskResponse(BaseModel):
output_width: int = 1280 output_width: int = 1280
output_height: int = 720 output_height: int = 720
cover_url: str = "" cover_url: str = ""
custom_title: str = "" title_config: dict = Field(default_factory=dict)
status: str status: str
progress: float progress: float
result_count: int result_count: int
+16
View File
@@ -101,3 +101,19 @@ class SaveToLibraryResponse(BaseModel):
voice_id: str voice_id: str
voice_name: str voice_name: str
status: str status: str
class TTSPreviewRequest(BaseModel):
"""TTS 预览(试听)请求。"""
text: str = Field(..., min_length=1, max_length=200, description="合成文本,限制 200 字")
voice_id: str = Field(..., min_length=1, description="音色 ID")
speed: float = Field(1.0, ge=0.5, le=2.0, description="语速")
pitch: float = Field(1.0, ge=0.5, le=2.0, description="音调(预留,当前未使用)")
class TTSPreviewResponse(BaseModel):
"""TTS 预览(试听)响应。"""
audio_url: str = Field(..., description="合成音频 URL")
duration: Optional[float] = Field(default=None, description="音频时长(秒)")
@@ -371,6 +371,88 @@ class EditPlanService:
logger.info("删除所有片段: plan_id=%s count=%d", plan_id, count) logger.info("删除所有片段: plan_id=%s count=%d", plan_id, count)
return count return count
def replace_all_clips_transactional(
self,
plan_id: str,
clips_data: list[dict],
) -> int:
"""事务性地替换所有片段:清空→创建→标记ready,单事务保证原子性。
Args:
plan_id: 计划 ID
clips_data: 片段数据列表,每项包含 asset_id/start_time/duration/order
Returns:
int: 创建的片段数量
Raises:
Exception: 任何步骤失败时自动回滚
"""
from packages.adapters.sqlalchemy_impl.models import EditPlanClipModel
db = self._clip_repo.session
try:
# 1. 清空现有 clips(不 commit
deleted_count = db.query(EditPlanClipModel).filter(EditPlanClipModel.plan_id == plan_id).delete()
# 2. 批量创建新 clips(不 commit
for i, clip_item in enumerate(clips_data):
order = clip_item.get("order") or i
clip = EditPlanClip.create(
plan_id=plan_id,
clip_type="main",
order=order,
asset_id=clip_item.get("asset_id", ""),
start_time=clip_item.get("start_time", 0.0),
duration=clip_item.get("duration", 0.0),
)
model = EditPlanClipModel(
id=clip.id,
plan_id=clip.plan_id,
clip_type=clip.clip_type,
order=clip.order,
asset_id=clip.asset_id,
text_content=clip.text_content,
start_time=clip.start_time,
duration=clip.duration,
transition_effect=clip.transition_effect,
transition_duration=clip.transition_duration,
playback_speed=clip.playback_speed,
status=clip.status.value,
config=clip.config,
)
db.add(model)
# flush 让新建 clip 写入当前事务(未 commit),后续查询才能找到它们
db.flush()
# 3. 标记有 asset_id 的 clips 为 ready(不 commit
pending_with_asset = (
db.query(EditPlanClipModel)
.filter(
EditPlanClipModel.plan_id == plan_id,
EditPlanClipModel.status == "pending",
EditPlanClipModel.asset_id != "",
)
.all()
)
for m in pending_with_asset:
m.status = "ready"
# 4. 一次性提交
db.commit()
logger.info(
"事务性替换片段: plan_id=%s deleted=%d created=%d",
plan_id,
deleted_count,
len(clips_data),
)
return len(clips_data)
except Exception:
db.rollback()
logger.exception("事务性替换片段失败: plan_id=%s", plan_id)
raise
# ── 片段分割与合并 ────────────────────────────────────────────────────── # ── 片段分割与合并 ──────────────────────────────────────────────────────
def split_clip(self, clip_id: str, split_time: float) -> Dict[str, Any]: def split_clip(self, clip_id: str, split_time: float) -> Dict[str, Any]:
+1 -4
View File
@@ -231,10 +231,7 @@ test.describe("Core generation flow", () => {
(response) => { (response) => {
const url = response.url() const url = response.url()
const path = new URL(url).pathname const path = new URL(url).pathname
return ( return response.request().method() === "POST" && path.endsWith("/generation/tasks")
response.request().method() === "POST" &&
path.endsWith("/generation/tasks")
)
}, },
{ timeout: 30_000 }, { timeout: 30_000 },
) )
+12 -3
View File
@@ -4,11 +4,20 @@
import apiClient from "../client" import apiClient from "../client"
import type { BgmPreset, BgmPresetsQuery } from "./types" import type { BgmPreset, BgmPresetsQuery } from "./types"
/** 获取 BGM 预设列表 */ /**
export const getBgmPresets = async (params?: BgmPresetsQuery): Promise<BgmPreset[]> => { * 获取 BGM 预设列表
* @param templateId 模板/草稿 ID
* @param params 分类/关键词筛选
*/
export const getBgmPresets = async (
templateId: string,
params?: BgmPresetsQuery,
): Promise<BgmPreset[]> => {
const searchParams: Record<string, string> = {} const searchParams: Record<string, string> = {}
if (params?.category) searchParams.category = params.category if (params?.category) searchParams.category = params.category
if (params?.keyword) searchParams.keyword = params.keyword if (params?.keyword) searchParams.keyword = params.keyword
const res = await apiClient.get("/bgm/presets", { params: searchParams }) const res = await apiClient.get(`/templates/${templateId}/editor/bgm/presets`, {
params: searchParams,
})
return res.data?.data ?? res.data ?? [] return res.data?.data ?? res.data ?? []
} }
+13
View File
@@ -1,9 +1,22 @@
import apiClient from "../client" import apiClient from "../client"
export interface GenerateCoverTitleConfig {
text?: string
font?: string
font_size?: number
font_color?: string
position?: string
bold?: boolean
stroke?: boolean
shadow?: boolean
}
export interface GenerateCoverRequest { export interface GenerateCoverRequest {
asset_ids: string[] asset_ids: string[]
cover_type?: "ai_frame" | "manual" | "upload" | "ai_regenerate" cover_type?: "ai_frame" | "manual" | "upload" | "ai_regenerate"
frame_time?: number frame_time?: number
/** 标题样式,用于在封面上叠加标题文字 */
title_config?: GenerateCoverTitleConfig
} }
export interface GenerateCoverResponse { export interface GenerateCoverResponse {
+6
View File
@@ -13,6 +13,10 @@ export interface CreatePreviewRequest {
video_title?: string video_title?: string
duration?: number duration?: number
video_ratio?: string video_ratio?: string
/** 输出视频宽度(与 video_ratio 匹配,如 9:16 → 1080 */
output_width?: number
/** 输出视频高度(与 video_ratio 匹配,如 9:16 → 1920 */
output_height?: number
/* 标题烧录配置(可选,传入后 ASS 渲染标题到预览视频中) */ /* 标题烧录配置(可选,传入后 ASS 渲染标题到预览视频中) */
title_config?: { title_config?: {
text?: string text?: string
@@ -38,6 +42,8 @@ export interface CreatePreviewResponse {
is_preview: boolean is_preview: boolean
resolution: string resolution: string
created_at: string created_at: string
/** 后端自动关联的编辑计划 ID(用于 fallback 路径传递 source_edit_plan_id */
source_edit_plan_id?: string
} }
/** 预览任务详情响应 */ /** 预览任务详情响应 */
+7
View File
@@ -6,6 +6,7 @@ import apiClient from "../client"
import type { import type {
CreateGenerationTaskRequest, CreateGenerationTaskRequest,
CreateGenerationTaskResponse, CreateGenerationTaskResponse,
GenerationTaskDetail,
TaskItem, TaskItem,
TaskListParams, TaskListParams,
TaskListResponse, TaskListResponse,
@@ -19,6 +20,12 @@ export const createGenerationTask = async (
return data return data
} }
/** 获取单个生成任务详情(轮询用) */
export const getGenerationTask = async (taskId: string): Promise<GenerationTaskDetail> => {
const { data } = await apiClient.get<GenerationTaskDetail>(`/generation/tasks/${taskId}`)
return data
}
/** 获取任务列表(支持分页和筛选) */ /** 获取任务列表(支持分页和筛选) */
export const getTasks = async (params?: TaskListParams): Promise<TaskListResponse> => { export const getTasks = async (params?: TaskListParams): Promise<TaskListResponse> => {
const { data } = await apiClient.get<TaskListResponse>("/tasks", { const { data } = await apiClient.get<TaskListResponse>("/tasks", {
+22 -2
View File
@@ -82,10 +82,20 @@ export interface CreateGenerationTaskRequest {
stroke?: boolean stroke?: boolean
shadow?: boolean shadow?: boolean
} }
/** 关联的草稿 ID(编辑流程数据链路用) */
source_edit_plan_id?: string
/** 配音素材库 ID(用户上传的音频或 AI 配音素材) */
voice_library_id?: string
/** 自定义 BGM 配置,覆盖模板 BGM 设置 */
bgm_config?: {
enabled: boolean
preset_id?: string
volume?: number
}
} }
/** 创建生成任务响应(对齐后端 GenerationTaskResponse */ /** 单个生成任务详情(对齐后端 GenerationTaskResponse */
export interface CreateGenerationTaskResponse { export interface GenerationTaskDetail {
id: string id: string
project_id: string project_id: string
asset_library_id: string asset_library_id: string
@@ -95,8 +105,18 @@ export interface CreateGenerationTaskResponse {
asset_ids: string[] asset_ids: string[]
title_ids: string[] title_ids: string[]
voice_ids: string[] voice_ids: string[]
source_edit_plan_id?: string
status: string status: string
progress: number progress: number
result_count: number result_count: number
error_message: string error_message: string
error_info?: TaskErrorInfo
created_at?: string | null
updated_at?: string | null
}
/** 创建生成任务响应(后端返回批量结构 {items, total} */
export interface CreateGenerationTaskResponse {
items: GenerationTaskDetail[]
total: number
} }
+24 -62
View File
@@ -1,27 +1,8 @@
/** /**
* 模板草稿 CRUD + 生成相关 API * 模板草稿 CRUD API
*/ */
import apiClient from "../client" import apiClient from "../client"
import type { import type { EditPlan, UpdateEditPlanRequest, GeneratedVideo } from "./types"
EditPlan,
EditPlanListParams,
EditPlanListResponse,
CreateEditPlanRequest,
UpdateEditPlanRequest,
GenerateResponse,
GenerationStatusResponse,
EditPlanGeneration,
GeneratedVideo,
CopyEditPlanRequest,
} from "./types"
/** 获取模板草稿列表(支持分页和筛选) */
export async function getEditPlans(params?: EditPlanListParams): Promise<EditPlanListResponse> {
const response = await apiClient.get<EditPlanListResponse>("/templates/drafts", {
params,
})
return response.data
}
/** 获取单个模板草稿 */ /** 获取单个模板草稿 */
export async function getEditPlan(templateId: string): Promise<EditPlan> { export async function getEditPlan(templateId: string): Promise<EditPlan> {
@@ -29,63 +10,44 @@ export async function getEditPlan(templateId: string): Promise<EditPlan> {
return response.data return response.data
} }
/** 创建模板草稿 */ /** 更新模板草稿(支持传入 AbortSignal 用于自动保存竞态取消) */
export async function createEditPlan(data: CreateEditPlanRequest): Promise<EditPlan> {
const response = await apiClient.post("/templates/drafts", data)
return response.data
}
/** 更新模板草稿 */
export async function updateEditPlan( export async function updateEditPlan(
templateId: string, templateId: string,
data: UpdateEditPlanRequest, data: UpdateEditPlanRequest,
signal?: AbortSignal,
): Promise<EditPlan> { ): Promise<EditPlan> {
const response = await apiClient.put(`/templates/${templateId}/editor`, data) const response = await apiClient.put(`/templates/${templateId}/editor`, data, { signal })
return response.data return response.data
} }
/** 删除模板草稿 */
export async function deleteEditPlan(templateId: string): Promise<void> {
await apiClient.delete(`/templates/${templateId}/editor`)
}
/** 触发生成 */
export async function generateEditPlan(templateId: string): Promise<GenerateResponse> {
const response = await apiClient.post(`/templates/${templateId}/editor/generate`)
return response.data
}
/** 获取生成状态(轮询用) */
export async function getGenerationStatus(templateId: string): Promise<GenerationStatusResponse> {
const response = await apiClient.get(`/templates/${templateId}/editor/generation-status`)
return response.data
}
/** 获取模板草稿关联的生成记录 */
export async function getEditPlanGenerations(templateId: string): Promise<EditPlanGeneration[]> {
const response = await apiClient.get(`/templates/${templateId}/editor/generations`)
return response.data.items || []
}
/** 获取生成任务的视频结果列表 */ /** 获取生成任务的视频结果列表 */
export async function getGenerationTaskResults(taskId: string): Promise<GeneratedVideo[]> { export async function getGenerationTaskResults(taskId: string): Promise<GeneratedVideo[]> {
const response = await apiClient.get(`/generation/tasks/${taskId}/results`) const response = await apiClient.get(`/generation/tasks/${taskId}/results`)
return response.data.items || response.data || [] return response.data.items || response.data || []
} }
/** 取消生成任务 */ /** ── 草稿 clips 批量更新 ── */
export async function cancelGeneration(templateId: string): Promise<void> {
await apiClient.post(`/templates/${templateId}/editor/cancel`) export interface EditPlanClipInput {
asset_id: string
start_time: number
duration: number
order: number
} }
/** 复制模板草稿(含所有片段配置) */ /**
export async function copyEditPlan( * 批量替换草稿的 clips(先全删再批量插入)
* 后端路由:PUT /templates/{template_id}/editor/clips
*/
export async function updateEditPlanClips(
templateId: string, templateId: string,
data?: CopyEditPlanRequest, clips: EditPlanClipInput[],
): Promise<EditPlan> { signal?: AbortSignal,
const response = await apiClient.post<EditPlan>( ): Promise<{ count: number }> {
`/templates/${templateId}/editor/copy`, const response = await apiClient.put(
data || {}, `/templates/${templateId}/editor/clips`,
{ clips },
{ signal },
) )
return response.data return response.data
} }
+2 -15
View File
@@ -15,14 +15,8 @@ export type {
EditPlanSegment, EditPlanSegment,
EditPlanConfig, EditPlanConfig,
EditPlan, EditPlan,
CreateEditPlanRequest,
UpdateEditPlanRequest, UpdateEditPlanRequest,
EditPlanListParams,
EditPlanListResponse,
GenerateResponse,
EditPlanGeneration,
ClipStatusItem, ClipStatusItem,
GenerationStatusResponse,
GeneratedVideo, GeneratedVideo,
AIRecommendRequest, AIRecommendRequest,
AIRecommendClipItem, AIRecommendClipItem,
@@ -37,7 +31,6 @@ export type {
ClipReorderResponse, ClipReorderResponse,
ClipBatchDeleteResponse, ClipBatchDeleteResponse,
ClipsFromAssetsResponse, ClipsFromAssetsResponse,
CopyEditPlanRequest,
TransitionEffect, TransitionEffect,
MediaAsset, MediaAsset,
} from "./types" } from "./types"
@@ -53,18 +46,12 @@ export {
// 模板草稿 CRUD + 生成 // 模板草稿 CRUD + 生成
export { export {
getEditPlans,
getEditPlan, getEditPlan,
createEditPlan,
updateEditPlan, updateEditPlan,
deleteEditPlan, updateEditPlanClips,
generateEditPlan,
getGenerationStatus,
getEditPlanGenerations,
getGenerationTaskResults, getGenerationTaskResults,
cancelGeneration,
copyEditPlan,
} from "./editPlans" } from "./editPlans"
export type { EditPlanClipInput } from "./editPlans"
// 片段 CRUD + 批量操作 // 片段 CRUD + 批量操作
export { export {
+11 -36
View File
@@ -118,6 +118,17 @@ export interface EditPlanConfig {
generate_count?: number generate_count?: number
/** 素材模式 */ /** 素材模式 */
material_mode?: string material_mode?: string
/** 前端标题设置(Step4 自动保存,与 title_config 字段分离,不影响后端渲染) */
title?: {
text?: string
font?: string
font_size?: number
color?: string
position?: string
bold?: boolean
stroke?: boolean
shadow?: boolean
}
/** 预览视频 URL(封面生成用) */ /** 预览视频 URL(封面生成用) */
rendered_storage_key?: string rendered_storage_key?: string
/** 生成任务 ID */ /** 生成任务 ID */
@@ -176,31 +187,6 @@ export interface EditPlanListResponse {
/* ── 生成相关 ── */ /* ── 生成相关 ── */
/** 生成响应 */
export interface GenerateResponse {
plan_id: string
plan_status: EditPlanStatus
generation_task_id: string
clip_count: number
}
/** 模板草稿关联的生成记录 */
export interface EditPlanGeneration {
id: string
source_edit_plan_id: string
template_id: string
asset_ids: string[]
status: EditPlanStatus
progress: number
result_count: number
error_message: string
error_info: Record<string, unknown>
logs: Array<Record<string, unknown>>
retry_count: number
created_at?: string
updated_at?: string
}
/** 片段生成状态 */ /** 片段生成状态 */
export interface ClipStatusItem { export interface ClipStatusItem {
clip_id: string clip_id: string
@@ -213,17 +199,6 @@ export interface ClipStatusItem {
error_message?: string error_message?: string
} }
/** 生成状态轮询响应 */
export interface GenerationStatusResponse {
plan_id: string
plan_status: EditPlanStatus
generation_task_id?: string
error_message?: string
clips: ClipStatusItem[]
error?: string
message?: string
}
/** 生成视频详情 */ /** 生成视频详情 */
export interface GeneratedVideo { export interface GeneratedVideo {
id: string id: string
-3
View File
@@ -9,8 +9,6 @@ export type {
TemplateSegment, TemplateSegment,
TemplateListParams, TemplateListParams,
TemplateListResponse, TemplateListResponse,
GenerateFromTemplateRequest,
GenerateFromTemplateResponse,
CopyTemplateResponse, CopyTemplateResponse,
} from "./types" } from "./types"
@@ -24,5 +22,4 @@ export {
getTemplate, getTemplate,
toggleFavoriteTemplate, toggleFavoriteTemplate,
copyTemplate, copyTemplate,
generateFromTemplate,
} from "./templates" } from "./templates"
-14
View File
@@ -5,8 +5,6 @@
import apiClient from "../client" import apiClient from "../client"
import type { import type {
CopyTemplateResponse, CopyTemplateResponse,
GenerateFromTemplateRequest,
GenerateFromTemplateResponse,
TemplateItem, TemplateItem,
TemplateListParams, TemplateListParams,
TemplateListResponse, TemplateListResponse,
@@ -45,15 +43,3 @@ export const copyTemplate = async (templateId: string): Promise<CopyTemplateResp
const response = await apiClient.post<CopyTemplateResponse>(`/templates/${templateId}/copy`) const response = await apiClient.post<CopyTemplateResponse>(`/templates/${templateId}/copy`)
return response.data return response.data
} }
/** 从模板生成 */
export const generateFromTemplate = async (
templateId: string,
data?: GenerateFromTemplateRequest,
): Promise<GenerateFromTemplateResponse> => {
const response = await apiClient.post<GenerateFromTemplateResponse>(
`/templates/${templateId}/generate`,
data,
)
return response.data
}
+2 -2
View File
@@ -71,7 +71,7 @@ export const NAV_ITEMS: NavItem[] = [
}, },
{ {
key: "editing-planner", key: "editing-planner",
label: "剪辑编辑器", label: "剪辑模板",
path: "/app/editing-planner", path: "/app/editing-planner",
icon: React.createElement(EditOutlined), icon: React.createElement(EditOutlined),
}, },
@@ -133,7 +133,7 @@ export const NAV_GROUPS: NavGroup[] = [
}, },
{ {
key: "editing-planner", key: "editing-planner",
label: "剪辑编辑器", label: "剪辑模板",
path: "/app/editing-planner", path: "/app/editing-planner",
icon: React.createElement(EditOutlined), icon: React.createElement(EditOutlined),
}, },
@@ -1,116 +0,0 @@
/**
* 生成历史弹窗 — 展示当前模板草稿的生成任务记录
* 从 EditingPlanner 拆分,避免主文件过大
*/
import React from "react"
import { CloseOutlined, InboxOutlined } from "@ant-design/icons"
import type { EditPlanGeneration } from "@/api/template-editor"
import { PLAN_STATUS_LABELS } from "@/api/template-editor"
interface GenerationHistoryModalProps {
open: boolean
loading: boolean
history: EditPlanGeneration[]
onClose: () => void
onCancel?: (taskId: string) => void
cancelLoading?: boolean
}
const GenerationHistoryModal: React.FC<GenerationHistoryModalProps> = ({
open,
loading,
history,
onClose,
onCancel,
cancelLoading,
}) => {
if (!open) return null
return (
<div className="ep-modal-overlay" onClick={onClose}>
<div className="ep-modal ep-gh-modal" onClick={(e) => e.stopPropagation()}>
<div className="ep-modal-header">
<h3></h3>
<button className="ep-modal-close" onClick={onClose}>
<CloseOutlined />
</button>
</div>
<div className="ep-modal-body ep-gh-body">
{loading ? (
<div className="ep-gh-empty">
<div className="ep-skeleton">
<div className="ep-skeleton-item ep-skeleton-item--header" />
<div className="ep-skeleton-item" />
<div className="ep-skeleton-item" />
<div className="ep-skeleton-item" />
</div>
</div>
) : history.length === 0 ? (
<div className="ep-gh-empty">
<InboxOutlined style={{ fontSize: 32, opacity: 0.4 }} />
<span></span>
</div>
) : (
<table className="ep-gh-table">
<thead>
<tr className="ep-gh-table-header-row">
<th className="ep-gh-th">ID</th>
<th className="ep-gh-th"></th>
<th className="ep-gh-th"></th>
<th className="ep-gh-th"></th>
{onCancel && <th className="ep-gh-th"></th>}
</tr>
</thead>
<tbody>
{history.map((gen) => {
const statusClass = `ep-gh-status-tag--${gen.status}`
const canCancel = gen.status === "rendering" || gen.status === "editing"
return (
<tr key={gen.id} className="ep-gh-table-row">
<td className="ep-gh-td ep-gh-td-id">
{gen.id ? `${gen.id.slice(0, 8)}...` : "—"}
</td>
<td className="ep-gh-td">
<span className={`ep-gh-status-tag ${statusClass}`}>
{PLAN_STATUS_LABELS[gen.status] || gen.status}
</span>
</td>
<td className="ep-gh-td ep-gh-td-time">
{gen.created_at ? new Date(gen.created_at).toLocaleString("zh-CN") : "—"}
</td>
<td className="ep-gh-td ep-gh-td-time">
{gen.updated_at ? new Date(gen.updated_at).toLocaleString("zh-CN") : "—"}
</td>
{onCancel && (
<td className="ep-gh-td ep-gh-td-action">
{canCancel ? (
<button
className="ep-gh-cancel-btn"
onClick={() => onCancel(gen.id)}
disabled={cancelLoading}
>
</button>
) : (
<span className="ep-gh-action-placeholder"></span>
)}
</td>
)}
</tr>
)
})}
</tbody>
</table>
)}
</div>
<div className="ep-modal-footer">
<button className="ep-btn ep-btn-secondary" onClick={onClose}>
</button>
</div>
</div>
</div>
)
}
export default GenerationHistoryModal
@@ -1,6 +0,0 @@
/**
* 生成进度弹窗 — 入口文件(向后兼容)
* 实际实现已移至 ./generation-progress-modal/ 目录
*/
export { default } from "./generation-progress-modal"
export type { GenPhase, GenerationProgressModalProps } from "./generation-progress-modal"
@@ -16,9 +16,17 @@ interface BgmSelectorProps {
onClose: () => void onClose: () => void
config: BgmMixConfig config: BgmMixConfig
onChange: (config: BgmMixConfig) => void onChange: (config: BgmMixConfig) => void
/** 模板/草稿 ID,用于请求 BGM 预设 */
templateId?: string
} }
const BgmSelector: React.FC<BgmSelectorProps> = ({ open, onClose, config, onChange }) => { const BgmSelector: React.FC<BgmSelectorProps> = ({
open,
onClose,
config,
onChange,
templateId,
}) => {
const { const {
presets, presets,
loading, loading,
@@ -30,7 +38,7 @@ const BgmSelector: React.FC<BgmSelectorProps> = ({ open, onClose, config, onChan
loadPresets, loadPresets,
handlePreview, handlePreview,
stopPreview, stopPreview,
} = useBgmSelector(open) } = useBgmSelector(open, templateId)
/* ── 选中 BGM ── */ /* ── 选中 BGM ── */
const handleSelect = useCallback( const handleSelect = useCallback(
@@ -19,7 +19,7 @@ export const CATEGORY_LIST: {
* BGM 选择器数据与交互 Hook * BGM 选择器数据与交互 Hook
* 封装列表加载、搜索、分类筛选、试听播放逻辑 * 封装列表加载、搜索、分类筛选、试听播放逻辑
*/ */
export function useBgmSelector(open: boolean) { export function useBgmSelector(open: boolean, templateId?: string) {
const [presets, setPresets] = useState<BgmPreset[]>([]) const [presets, setPresets] = useState<BgmPreset[]>([])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [activeCategory, setActiveCategory] = useState<BgmCategory | "all">("all") const [activeCategory, setActiveCategory] = useState<BgmCategory | "all">("all")
@@ -30,19 +30,23 @@ export function useBgmSelector(open: boolean) {
/* ── 加载 BGM 列表 ── */ /* ── 加载 BGM 列表 ── */
const loadPresets = useCallback(async () => { const loadPresets = useCallback(async () => {
if (!templateId) {
setPresets([])
return
}
setLoading(true) setLoading(true)
try { try {
const params: { category?: string; keyword?: string } = {} const params: { category?: string; keyword?: string } = {}
if (activeCategory !== "all") params.category = activeCategory if (activeCategory !== "all") params.category = activeCategory
if (keyword.trim()) params.keyword = keyword.trim() if (keyword.trim()) params.keyword = keyword.trim()
const data = await getBgmPresets(params) const data = await getBgmPresets(templateId, params)
setPresets(data) setPresets(data)
} catch { } catch {
message.error("加载 BGM 列表失败") message.error("加载 BGM 列表失败")
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [activeCategory, keyword]) }, [activeCategory, keyword, templateId])
useEffect(() => { useEffect(() => {
if (open) loadPresets() if (open) loadPresets()
@@ -1,65 +0,0 @@
import React from "react"
import { Modal } from "@/components/ui"
import type { TaskItem } from "@/api/tasks"
import { getStepLabel, getStatusColor } from "./constants"
interface ProgressPhaseProps {
open: boolean
task: TaskItem | null
onCancel: () => void
}
/** progress(进度轮询)阶段弹窗 */
export const ProgressPhase: React.FC<ProgressPhaseProps> = ({ open, task, onCancel }) => {
const progress = task?.progress ?? 0
const status = task?.status ?? ""
const currentStep = task?.current_step ?? ""
const userMessage = task?.user_message ?? ""
const stepColor = getStatusColor(status, currentStep)
return (
<Modal open={open} title="视频生成中" footer={null} onCancel={onCancel} closable width={480}>
<div className="ep-gen-progress">
{/* 进度环 */}
<div className="ep-gen-progress-ring-wrap">
<svg className="ep-gen-progress-ring" viewBox="0 0 120 120">
<circle className="ep-gen-progress-ring-bg" cx="60" cy="60" r="52" />
<circle
className="ep-gen-progress-ring-fill"
cx="60"
cy="60"
r="52"
style={{
strokeDasharray: `${2 * Math.PI * 52}`,
strokeDashoffset: `${2 * Math.PI * 52 * (1 - progress / 100)}`,
stroke: stepColor,
}}
/>
</svg>
<span className="ep-gen-progress-pct" style={{ color: stepColor }}>
{progress}%
</span>
</div>
{/* 当前步骤 */}
<div className="ep-gen-step-text">
{userMessage || getStepLabel(currentStep) || "处理中…"}
</div>
{/* 进度条 */}
<div className="ep-gen-progress-bar">
<div
className="ep-gen-progress-bar-fill"
style={{
width: `${progress}%`,
backgroundColor: stepColor,
}}
/>
</div>
{/* 任务 ID */}
{task?.id && <div className="ep-gen-task-id"> ID: {task.id}</div>}
</div>
</Modal>
)
}
@@ -1,82 +0,0 @@
import React from "react"
import { Modal, Button } from "@/components/ui"
import type { TaskItem } from "@/api/tasks"
interface ResultPhaseProps {
open: boolean
phase: "completed" | "failed"
task: TaskItem | null
onCancel: () => void
onRetry?: () => void
onClose?: () => void
}
/** completed / failed(结果)阶段弹窗 */
export const ResultPhase: React.FC<ResultPhaseProps> = ({
open,
phase,
task,
onCancel,
onRetry,
onClose,
}) => {
const userMessage = task?.user_message ?? ""
const errorMessage = task?.error_message ?? ""
const retryable = task?.retryable ?? false
const handleClose = onClose || onCancel
if (phase === "completed") {
return (
<Modal
open={open}
title="✅ 生成完成"
footer={null}
onCancel={handleClose}
closable
width={440}
>
<div className="ep-gen-result">
<div className="ep-gen-result-icon">🎉</div>
<div className="ep-gen-result-title"></div>
{userMessage && <div className="ep-gen-result-msg">{userMessage}</div>}
<div className="ep-gen-result-actions">
<Button buttonType="primary" onClick={handleClose}>
</Button>
</div>
</div>
</Modal>
)
}
return (
<Modal
open={open}
title="❌ 生成失败"
footer={null}
onCancel={handleClose}
closable
width={440}
>
<div className="ep-gen-result ep-gen-result--error">
<div className="ep-gen-result-icon">😥</div>
<div className="ep-gen-result-title"></div>
{(errorMessage || userMessage) && (
<div className="ep-gen-result-msg ep-gen-result-msg--error">
{errorMessage || userMessage}
</div>
)}
<div className="ep-gen-result-actions">
{retryable && onRetry && (
<Button buttonType="primary" onClick={onRetry}>
🔄
</Button>
)}
<Button buttonType="secondary" onClick={handleClose}>
</Button>
</div>
</div>
</Modal>
)
}
@@ -1,53 +0,0 @@
import React from "react"
import { Modal } from "@/components/ui"
interface SetupPhaseProps {
open: boolean
voiceoverDuration: number | null
estimatedDuration: number
submitting: boolean
onDurationChange: (v: number | null) => void
onGenerate: () => void
onCancel: () => void
}
/** setup(配置)阶段弹窗 */
export const SetupPhase: React.FC<SetupPhaseProps> = ({
open,
voiceoverDuration,
estimatedDuration,
submitting,
onDurationChange,
onGenerate,
onCancel,
}) => (
<Modal
open={open}
title="使用模板生成视频"
confirmLoading={submitting}
onOk={onGenerate}
onCancel={onCancel}
okText="开始生成"
cancelText="取消"
width={440}
>
<div className="ep-gen-setup">
<label className="ep-gen-field-label"></label>
<input
type="number"
className="ep-gen-duration-input"
placeholder="请输入配音时长"
value={voiceoverDuration ?? ""}
onChange={(e) => {
const v = e.target.value ? Number(e.target.value) : null
onDurationChange(v)
}}
min={1}
max={600}
/>
<div className="ep-gen-estimate">
<strong>{estimatedDuration}s</strong>
</div>
</div>
</Modal>
)
@@ -1,31 +0,0 @@
/* ──────────── 步骤文案映射 ──────────── */
export const STEP_LABELS: Record<string, string> = {
queued: "排队中…",
preparing: "准备素材…",
generating_video: "渲染视频中…",
adding_effects: "添加特效…",
composing: "合成中…",
encoding: "编码输出中…",
completed: "生成完成!",
failed: "生成失败",
}
export const getStepLabel = (step: string) => STEP_LABELS[step] || step.replace(/_/g, " ")
/* ──────────── 状态徽标颜色 ──────────── */
export const STATUS_COLOR: Record<string, string> = {
queued: "#6b7280",
pending: "#6b7280",
preparing: "#f59e0b",
generating_video: "#4f46e5",
adding_effects: "#7c3aed",
composing: "#2563eb",
encoding: "#0891b2",
completed: "#10b981",
failed: "#ef4444",
}
export const getStatusColor = (status: string, currentStep: string) =>
STATUS_COLOR[status] || STATUS_COLOR[currentStep] || "#4f46e5"
@@ -1,70 +0,0 @@
/**
* 生成进度弹窗 — 任务 2.17
* 三阶段 UIsetup(配置)→ progress(进度轮询)→ completed / failed(结果)
* V21 设计系统,CSS 类名前缀 ep-gen-
*/
import React, { useEffect, useRef } from "react"
import type { GenPhase, GenerationProgressModalProps } from "./types"
import { SetupPhase } from "./SetupPhase"
import { ProgressPhase } from "./ProgressPhase"
import { ResultPhase } from "./ResultPhase"
/* 重新导出类型,保持向后兼容 */
export type { GenPhase, GenerationProgressModalProps }
const GenerationProgressModal: React.FC<GenerationProgressModalProps> = ({
open,
phase,
voiceoverDuration,
estimatedDuration,
onDurationChange,
onGenerate,
task,
submitting,
onCancel,
onRetry,
onClose,
}) => {
/* 关闭弹窗时重置(避免下次打开残留旧状态) */
const prevOpen = useRef(false)
useEffect(() => {
if (prevOpen.current && !open) {
/* modal just closed — parent handles reset */
}
prevOpen.current = open
}, [open])
/* setup 阶段 */
if (phase === "setup") {
return (
<SetupPhase
open={open}
voiceoverDuration={voiceoverDuration}
estimatedDuration={estimatedDuration}
submitting={submitting}
onDurationChange={onDurationChange}
onGenerate={onGenerate}
onCancel={onCancel}
/>
)
}
/* progress 阶段 */
if (phase === "progress") {
return <ProgressPhase open={open} task={task} onCancel={onCancel} />
}
/* completed / failed 阶段 */
return (
<ResultPhase
open={open}
phase={phase as "completed" | "failed"}
task={task}
onCancel={onCancel}
onRetry={onRetry}
onClose={onClose}
/>
)
}
export default GenerationProgressModal
@@ -1,23 +0,0 @@
import type { TaskItem } from "@/api/tasks"
export type GenPhase = "setup" | "progress" | "completed" | "failed"
export interface GenerationProgressModalProps {
open: boolean
phase: GenPhase
/* setup 阶段 */
voiceoverDuration: number | null
estimatedDuration: number
onDurationChange: (v: number | null) => void
onGenerate: () => void
/* progress / 结果阶段 */
task: TaskItem | null
/* 通用 */
submitting: boolean
onCancel: () => void
onRetry?: () => void
onClose?: () => void
}
+114 -48
View File
@@ -1,21 +1,19 @@
/** /**
* 智能剪辑页面 — V24 前端预览播放器架构改造 * 智能剪辑页面 — 前端实时预览架构
* 7 步向导:选择模板 → 素材 → 配音 → 标题 → 预览 → 封面 → 确认生成 * 7 步向导:选择模板 → 素材 → 配音 → 标题 → 预览 → 封面 → 确认生成
* 左右布局:左侧 generate-form + 右侧 generate-preview * 左右布局:左侧 generate-form + 右侧 generate-preview
* *
* 架构改造 * 架构:
* - Step5 预览改为前端素材切片播放(FrontendPreviewPlayer * - Step4+ 右侧预览面板使用 FrontendPreviewPlayer 实时播放素材片段
* - 完全去除后端 FFmpeg 预览依赖 * - 标题样式编辑时 CSS 层实时叠加预览,所见即所得
* - 标题样式通过 CSS 层实时叠加,所见即所得 * - 点"确认生成"时调用 createGenerationTask 创建一次服务器渲染任务
* - 最终成片仍走后端 FFmpeg 渲染(Step7 确认生成)
*/ */
import React, { useMemo } from "react" import React, { useMemo, useState, useEffect, useRef } from "react"
import { Modal, message } from "antd" import { Modal, message } from "antd"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import type { VoiceClone } from "@/api/voice-clone" import type { VoiceClone } from "@/api/voice-clone"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { useCloneProgress } from "@/hooks/useCloneProgress" import { useCloneProgress } from "@/hooks/useCloneProgress"
import { getAssetsByKind } from "@/api/assets"
import CloneModal from "@/components/voice/CloneModal" import CloneModal from "@/components/voice/CloneModal"
import GenerateHeader from "./components/GenerateHeader" import GenerateHeader from "./components/GenerateHeader"
import { import {
@@ -24,7 +22,7 @@ import {
} from "./utils/calculateTotalVideoDuration" } from "./utils/calculateTotalVideoDuration"
import GenerateStepsBar from "./components/GenerateStepsBar" import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateResultPanel from "./components/GenerateResultPanel" import GenerateResultPanel from "./components/GenerateResultPanel"
import PreviewVideoPanel from "./components/PreviewVideoPanel" import FrontendPreviewPlayer from "./components/FrontendPreviewPlayer"
import GenerateStepContent from "./components/GenerateStepContent" import GenerateStepContent from "./components/GenerateStepContent"
import GenerateStepActions from "./components/GenerateStepActions" import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState" import { useGenerateFormState } from "./hooks/useGenerateFormState"
@@ -32,6 +30,8 @@ import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo" import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { usePreviewAssets } from "./hooks/usePreviewAssets" import { usePreviewAssets } from "./hooks/usePreviewAssets"
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters" import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters"
import { getAssetsByKind } from "@/api/assets"
import { previewTts } from "@/api/tts"
import "./generate.css" import "./generate.css"
const GeneratePage: React.FC = () => { const GeneratePage: React.FC = () => {
@@ -72,18 +72,76 @@ const GeneratePage: React.FC = () => {
autoSubtitles, autoSubtitles,
bgm, bgm,
editPlanId, editPlanId,
sourceEditPlanId,
previewVideo, previewVideo,
setPreviewVideo, setPreviewVideo,
previewModalOpen, previewModalOpen,
setPreviewModalOpen, setPreviewModalOpen,
previewTaskId,
setPreviewTaskId,
storedSourceEditPlanId,
setStoredSourceEditPlanId,
} = formState } = formState
/* ── 标题样式回调(Step5 样式面板 + 右侧预览 CSS 层共用) ── */ /* ── 标题样式回调 ── */
const styleUpdaters = useTitleStyleUpdaters({ const styleUpdaters = useTitleStyleUpdaters({
titleSettings, titleSettings,
onTitleSettingsChange: setTitleSettings, onTitleSettingsChange: setTitleSettings,
}) })
/* ── 配音预览音频(TTS 试听)── */
const { data: voiceMaterials = [] } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
const [previewVoiceAudioUrl, setPreviewVoiceAudioUrl] = useState<string | null>(null)
const ttsAbortRef = useRef<AbortController | null>(null)
useEffect(() => {
// 如果 selectedVoice 是已上传的配音素材,直接用 file_url
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice)
if (voiceAsset?.file_url) {
setPreviewVoiceAudioUrl(voiceAsset.file_url)
return
}
// 没有选中的 voice 或标题,跳过
const voiceId = selectedClonedVoice || selectedVoice
if (!voiceId || !titleSettings.title) {
setPreviewVoiceAudioUrl(null)
return
}
// 预设音色 / 克隆音色 → 调 TTS 合成
ttsAbortRef.current?.abort()
const controller = new AbortController()
ttsAbortRef.current = controller
let cancelled = false
previewTts({
text: titleSettings.title,
voice_id: voiceId,
})
.then((res) => {
if (!cancelled && res.audio_url) {
setPreviewVoiceAudioUrl(res.audio_url)
}
})
.catch((err) => {
if (!cancelled) {
console.warn("[预览配音生成失败]", err)
setPreviewVoiceAudioUrl(null)
}
})
return () => {
cancelled = true
controller.abort()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedVoice, selectedClonedVoice, titleSettings.title, voiceMaterials])
/* ── 克隆声音 ── */ /* ── 克隆声音 ── */
const { clones: clonedVoices, addClone, hasProcessing } = useCloneProgress() const { clones: clonedVoices, addClone, hasProcessing } = useCloneProgress()
@@ -93,44 +151,38 @@ const GeneratePage: React.FC = () => {
message.success("音色克隆成功!") message.success("音色克隆成功!")
} }
/* ── 前端预览:加载选中素材的视频文件信息 ── */ /* ── 素材 ID 列表 ── */
const previewAssetIds = useMemo( const previewAssetIds = useMemo(
() => (materialMode === "auto" ? smartSelectedIds : selectedMaterials), () => (materialMode === "auto" ? smartSelectedIds : selectedMaterials),
[materialMode, smartSelectedIds, selectedMaterials], [materialMode, smartSelectedIds, selectedMaterials],
) )
const previewAssetsEnabled = currentStep >= 4 && previewAssetIds.length > 0
const {
assets: previewAssets,
loading: previewAssetsLoading,
ready: previewAssetsReady,
} = usePreviewAssets(previewAssetIds, previewAssetsEnabled)
/* ── 当前模板对象(传给前端预览播放器) ── */ /* ── 当前模板对象 ── */
const currentTemplate = useMemo( const currentTemplate = useMemo(
() => userTemplates.find((t) => t.id === selectedTemplate) || null, () => userTemplates.find((t) => t.id === selectedTemplate) || null,
[userTemplates, selectedTemplate], [userTemplates, selectedTemplate],
) )
/* ── 视频总时长计算(用于配音时长校验) ── */ /* ── BGM 配置 ── */
const bgmConfig = useMemo(
() => ({
enabled: bgm,
music_id: currentTemplate?.bgm_config?.music_id || "",
}),
[bgm, currentTemplate],
)
/* ── 加载素材详情(供前端预览播放器使用 + 配音时长校验) ── */
const previewAssetsEnabled = previewAssetIds.length > 0
const { assets: previewAssets } = usePreviewAssets(previewAssetIds, previewAssetsEnabled)
/* ── 视频总时长计算 ── */
const totalVideoDuration = useMemo(() => { const totalVideoDuration = useMemo(() => {
// 优先用素材精确时长;素材未加载时用模板 segments 的 duration_max 之和估算
const exact = calculateTotalVideoDuration(previewAssets, currentTemplate ?? undefined) const exact = calculateTotalVideoDuration(previewAssets, currentTemplate ?? undefined)
if (exact > 0) return exact if (exact > 0) return exact
return estimateTotalVideoDuration(currentTemplate ?? undefined) return estimateTotalVideoDuration(currentTemplate ?? undefined)
}, [previewAssets, currentTemplate]) }, [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({ const { goNext, goPrev } = useStepNavigation({
currentStep, currentStep,
@@ -140,7 +192,6 @@ const GeneratePage: React.FC = () => {
selectedMaterials, selectedMaterials,
smartSelectedIds, smartSelectedIds,
titleSettings, titleSettings,
previewReady: previewAssetsReady,
}) })
/* ── 视频生成核心逻辑 ── */ /* ── 视频生成核心逻辑 ── */
@@ -171,21 +222,25 @@ const GeneratePage: React.FC = () => {
autoSubtitles, autoSubtitles,
bgm, bgm,
generateCount, generateCount,
sourceEditPlanId: storedSourceEditPlanId || sourceEditPlanId,
previewTaskId,
bgmConfig,
onGenerationSuccess: () => {
setPreviewTaskId(null)
setStoredSourceEditPlanId(null)
},
}) })
/* ================================================================ /* ================================================================
渲染 — 主页面 渲染
================================================================ */ ================================================================ */
return ( return (
<div className="xx-generate-page"> <div className="xx-generate-page">
{/* ── 页头 ── */}
<GenerateHeader fromEditPlan={!!editPlanId} /> <GenerateHeader fromEditPlan={!!editPlanId} />
{/* ── 步骤条 ── */}
<GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} /> <GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} />
{/* ── 主布局 ── */}
<div className="xx-generate-layout"> <div className="xx-generate-layout">
{/* ════ 左侧:表单区 ════ */} {/* ════ 左侧:表单区 ════ */}
<div className="xx-generate-form"> <div className="xx-generate-form">
@@ -202,7 +257,6 @@ const GeneratePage: React.FC = () => {
onSmartSelectedIdsChange={setSmartSelectedIds} onSmartSelectedIdsChange={setSmartSelectedIds}
titleSettings={titleSettings} titleSettings={titleSettings}
onTitleSettingsChange={setTitleSettings} onTitleSettingsChange={setTitleSettings}
/* 标题样式回调 */
onUpdatePosition={styleUpdaters.updatePosition} onUpdatePosition={styleUpdaters.updatePosition}
onUpdateFont={styleUpdaters.updateFont} onUpdateFont={styleUpdaters.updateFont}
onUpdateSize={styleUpdaters.updateSize} onUpdateSize={styleUpdaters.updateSize}
@@ -213,6 +267,10 @@ const GeneratePage: React.FC = () => {
onApplyPreset={styleUpdaters.applyPreset} onApplyPreset={styleUpdaters.applyPreset}
activePreset={styleUpdaters.activePreset} activePreset={styleUpdaters.activePreset}
titlePresets={styleUpdaters.titlePresets} titlePresets={styleUpdaters.titlePresets}
onPreviewTaskCreated={setPreviewTaskId}
onSourceEditPlanIdExtracted={setStoredSourceEditPlanId}
bgm={bgm}
bgmConfig={bgmConfig}
coverSettings={coverSettings} coverSettings={coverSettings}
onCoverSettingsChange={setCoverSettings} onCoverSettingsChange={setCoverSettings}
duration={duration} duration={duration}
@@ -251,18 +309,26 @@ const GeneratePage: React.FC = () => {
/> />
</div> </div>
{/* ════ 右侧:预览 + 生成结果 ════ */} {/* ════ 右侧:预览 + 结果 ════ */}
<div className="xx-generate-right-col"> <div className="xx-generate-right-col">
{/* 预览视频面板(Step4+ 显示,含 CSS 标题实时预览层) */} {currentStep >= 4 && !!currentTemplate && (
{currentStep >= 4 && ( <FrontendPreviewPlayer
<PreviewVideoPanel
assets={previewAssets} assets={previewAssets}
template={currentTemplate} template={currentTemplate}
videoRatio={videoRatio} videoRatio={videoRatio}
assetsReady={previewAssetsReady} ready={previewAssets.length > 0}
assetsLoading={previewAssetsLoading} voiceAudioUrl={previewVoiceAudioUrl || undefined}
titleSettings={titleSettings} titleSettings={{
voiceAudioUrl={voiceAudioUrl} title: titleSettings.title,
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as "top" | "center" | "bottom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
}}
/> />
)} )}
{currentStep >= 6 && ( {currentStep >= 6 && (
@@ -284,7 +350,7 @@ const GeneratePage: React.FC = () => {
</div> </div>
</div> </div>
{/* ── 视频预览弹窗 ── */} {/* 视频预览弹窗 */}
<Modal <Modal
className="xx-preview-modal" className="xx-preview-modal"
open={previewModalOpen} open={previewModalOpen}
@@ -307,7 +373,7 @@ const GeneratePage: React.FC = () => {
)} )}
</Modal> </Modal>
{/* ── 音色克隆弹窗 ── */} {/* 音色克隆弹窗 */}
<CloneModal <CloneModal
open={cloneModalOpen} open={cloneModalOpen}
onClose={() => setCloneModalOpen(false)} onClose={() => setCloneModalOpen(false)}
@@ -17,7 +17,7 @@ import {
import type { AssetItem } from "@/api/assets" import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner" import type { EditingTemplate } from "@/api/editing-planner"
import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler" import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler"
import { useCanvasPlayer, isWebCodecsSupported } from "../hooks/useCanvasPlayer" import { useCanvasPlayer } from "../hooks/useCanvasPlayer"
interface FrontendPreviewPlayerProps { interface FrontendPreviewPlayerProps {
assets: AssetItem[] assets: AssetItem[]
@@ -82,7 +82,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
titleSettings, titleSettings,
}) => { }) => {
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template]) const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
const useWebCodecs = isWebCodecsSupported() // 默认走原生 video 播放(浏览器硬件解码,独立线程,不阻塞 UI)
// WebCodecs 仅在明确需要时启用(保留代码作为兜底)
const useWebCodecs = false
// ── 两条路径共用同一个 canvas ref(fallback 路径不使用) ── // ── 两条路径共用同一个 canvas ref(fallback 路径不使用) ──
const canvasRef = useRef<HTMLCanvasElement>(null) const canvasRef = useRef<HTMLCanvasElement>(null)
@@ -112,12 +114,30 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
[segments], [segments],
) )
// WebCodecs 解码失败后强制走 video fallback
const [forceVideoFallback, setForceVideoFallback] = useState(false)
const handleCanvasError = useCallback((err: Error) => {
console.error("[FrontendPreviewPlayer] Canvas decode Error, switching to video fallback:", err)
setForceVideoFallback(true)
}, [])
const { state: canvasState, controls: canvasControls } = useCanvasPlayer( const { state: canvasState, controls: canvasControls } = useCanvasPlayer(
canvasRef, canvasRef,
canvasSegments, useWebCodecs && !forceVideoFallback ? canvasSegments : [],
useWebCodecs ? canvasTitle : undefined, useWebCodecs && !forceVideoFallback ? canvasTitle : undefined,
handleCanvasError,
useWebCodecs && !forceVideoFallback,
) )
// WebCodecs 报告解码失败时自动切换到 video fallback
useEffect(() => {
if (canvasState.hasDecodeError && !forceVideoFallback) {
console.warn("[FrontendPreviewPlayer] hasDecodeError detected, forcing video fallback")
setForceVideoFallback(true)
}
}, [canvasState.hasDecodeError, forceVideoFallback])
// ── Video 播放器(fallback 路径) ── // ── Video 播放器(fallback 路径) ──
const { const {
isPlaying: videoIsPlaying, isPlaying: videoIsPlaying,
@@ -130,12 +150,13 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
videoRefs, videoRefs,
} = useSegmentScheduler(segments) } = useSegmentScheduler(segments)
// 选择哪条路径的状态 // 选择哪条路径的状态(WebCodecs 解码失败时强制走 video fallback
const isPlaying = useWebCodecs ? canvasState.isPlaying : videoIsPlaying const effectiveUseWebCodecs = useWebCodecs && !forceVideoFallback
const currentTime = useWebCodecs ? canvasState.currentTime : videoCurrentTime const isPlaying = effectiveUseWebCodecs ? canvasState.isPlaying : videoIsPlaying
const totalDuration = useWebCodecs ? canvasState.duration : videoTotalDuration const currentTime = effectiveUseWebCodecs ? canvasState.currentTime : videoCurrentTime
const canPlay = useWebCodecs ? canvasState.isReady : videoCanPlay const totalDuration = effectiveUseWebCodecs ? canvasState.duration : videoTotalDuration
const isBuffering = useWebCodecs ? canvasState.isBuffering : false const canPlay = effectiveUseWebCodecs ? canvasState.isReady : videoCanPlay
const isBuffering = effectiveUseWebCodecs ? canvasState.isBuffering : false
// ── 配音音频同步 ── // ── 配音音频同步 ──
const audioRef = useRef<HTMLAudioElement | null>(null) const audioRef = useRef<HTMLAudioElement | null>(null)
@@ -172,16 +193,18 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
}, [isPlaying, currentTime]) }, [isPlaying, currentTime])
// 片段切换时同步音频(仅 fallback 路径需要) // 片段切换时同步音频(仅 fallback 路径需要)
const segmentSyncKey = useWebCodecs ? -1 : videoCurrentSegIdx const segmentSyncKey = effectiveUseWebCodecs ? -1 : videoCurrentSegIdx
useEffect(() => { useEffect(() => {
const audio = audioRef.current const audio = audioRef.current
if (!audio || !audio.src || !isPlaying) return if (!audio || !audio.src || !isPlaying) return
audio.currentTime = currentTime audio.currentTime = currentTime
}, [segmentSyncKey, isPlaying, currentTime]) // 注意:不要把 currentTime 放进依赖数组,否则每200ms会重置音频位置导致卡顿
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [segmentSyncKey, isPlaying])
const handleSeekTo = useCallback( const handleSeekTo = useCallback(
(time: number) => { (time: number) => {
if (useWebCodecs) { if (effectiveUseWebCodecs) {
canvasControls.seek(time) canvasControls.seek(time)
} else { } else {
videoSeekTo(time) videoSeekTo(time)
@@ -191,11 +214,11 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
audio.currentTime = time audio.currentTime = time
} }
}, },
[useWebCodecs, canvasControls, videoSeekTo], [effectiveUseWebCodecs, canvasControls, videoSeekTo],
) )
const handleTogglePlay = useCallback(() => { const handleTogglePlay = useCallback(() => {
if (useWebCodecs) { if (effectiveUseWebCodecs) {
if (canvasState.isPlaying) { if (canvasState.isPlaying) {
canvasControls.pause() canvasControls.pause()
} else { } else {
@@ -204,7 +227,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
} else { } else {
videoTogglePlayPause() videoTogglePlayPause()
} }
}, [useWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause]) }, [effectiveUseWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause])
// ── 进度条拖拽 ── // ── 进度条拖拽 ──
const [isDragging, setIsDragging] = useState(false) const [isDragging, setIsDragging] = useState(false)
@@ -247,82 +270,102 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0 const progressPercent = totalDuration > 0 ? (currentTime / totalDuration) * 100 : 0
// ── Canvas ResizeObserver ── // ── Canvas 容器 ref(保留声明,WebCodecs 兜底路径仍引用) ──
const canvasContainerRef = useRef<HTMLDivElement>(null) 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) { if (!ready || !assets.length) {
return ( return (
<div <div
className="xx-preview-empty"
style={{ style={{
position: "absolute", position: "relative",
inset: 0, width: "100%",
maxWidth: 280,
aspectRatio: "9 / 16",
background: "#0a0a0a",
borderRadius: 24,
overflow: "hidden",
boxShadow:
"0 4px 6px -1px rgba(0,0,0,0.3), 0 20px 50px -12px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.06)",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
zIndex: 1,
}} }}
> >
<SoundOutlined style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }} /> <SoundOutlined style={{ fontSize: 40, color: "rgba(255,255,255,0.3)", marginBottom: 12 }} />
<p className="xx-preview-empty-title">...</p> <p style={{ color: "rgba(255,255,255,0.6)", fontSize: 14, margin: "0 0 4px" }}>
<p className="xx-preview-empty-desc"></p> ...
</p>
<p style={{ color: "rgba(255,255,255,0.35)", fontSize: 12, margin: 0 }}>
</p>
</div> </div>
) )
} }
// ── 无播放片段 ── // ── 无播放片段 ──
if (!canPlay) { if (!canPlay) {
const showDecodeError = forceVideoFallback && canvasState.hasDecodeError
return ( return (
<div <div
className="xx-preview-empty"
style={{ style={{
position: "absolute", position: "relative",
inset: 0, width: "100%",
maxWidth: 280,
aspectRatio: "9 / 16",
background: "#0a0a0a",
borderRadius: 24,
overflow: "hidden",
boxShadow:
"0 4px 6px -1px rgba(0,0,0,0.3), 0 20px 50px -12px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.06)",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
zIndex: 1, padding: 24,
}} }}
> >
{isBuffering ? ( {isBuffering ? (
<> <>
<LoadingOutlined style={{ fontSize: 48, color: "#fff", marginBottom: 12 }} spin /> <LoadingOutlined style={{ fontSize: 40, color: "#fff", marginBottom: 12 }} spin />
<p style={{ color: "rgba(255,255,255,0.8)" }}>...</p> <p style={{ color: "rgba(255,255,255,0.8)", fontSize: 14, margin: 0 }}>...</p>
</>
) : showDecodeError ? (
<>
<PlayCircleOutlined style={{ fontSize: 40, color: "#ef4444", marginBottom: 12 }} />
<p
style={{
color: "rgba(255,255,255,0.9)",
fontSize: 14,
margin: "0 0 4px",
fontWeight: 500,
}}
>
</p>
<p
style={{
color: "rgba(255,255,255,0.5)",
fontSize: 12,
margin: 0,
textAlign: "center",
}}
>
{canvasState.errorMessage || "当前浏览器不支持该视频编码格式,请刷新重试"}
</p>
</> </>
) : ( ) : (
<> <>
<PlayCircleOutlined <PlayCircleOutlined
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }} style={{ fontSize: 40, color: "rgba(255,255,255,0.3)", marginBottom: 12 }}
/> />
<p className="xx-preview-empty-title"></p> <p style={{ color: "rgba(255,255,255,0.6)", fontSize: 14, margin: "0 0 4px" }}>
<p className="xx-preview-empty-desc"></p>
</p>
<p style={{ color: "rgba(255,255,255,0.35)", fontSize: 12, margin: 0 }}>
</p>
</> </>
)} )}
</div> </div>
@@ -330,9 +373,21 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
} }
return ( return (
<> <div
style={{
position: "relative",
width: "100%",
maxWidth: 280,
aspectRatio: "9 / 16",
background: "#0a0a0a",
borderRadius: 24,
overflow: "hidden",
boxShadow:
"0 4px 6px -1px rgba(0,0,0,0.3), 0 20px 50px -12px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.06)",
}}
>
{/* ── Canvas 渲染层(WebCodecs 路径) ── */} {/* ── Canvas 渲染层(WebCodecs 路径) ── */}
{useWebCodecs && ( {effectiveUseWebCodecs && (
<div <div
ref={canvasContainerRef} ref={canvasContainerRef}
style={{ style={{
@@ -353,8 +408,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
</div> </div>
)} )}
{/* ── Video 渲染层(fallback 路径 ── */} {/* ── Video 渲染层(默认路径,浏览器原生硬件解码 ── */}
{!useWebCodecs && {!effectiveUseWebCodecs &&
segments.map((seg, i) => ( segments.map((seg, i) => (
<video <video
key={seg.assetId} key={seg.assetId}
@@ -362,9 +417,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
ref={(el) => { ref={(el) => {
videoRefs.current[i] = el videoRefs.current[i] = el
}} }}
preload={ preload="auto"
i === videoCurrentSegIdx ? "auto" : i === videoCurrentSegIdx + 1 ? "metadata" : "none"
}
src={seg.videoUrl} src={seg.videoUrl}
style={{ style={{
position: "absolute", position: "absolute",
@@ -381,54 +434,114 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
/> />
))} ))}
{/* 播放按钮 */} {/* 标题CSS叠加层 — video fallback 路径也要渲染 */}
{titleSettings?.title && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
zIndex: 5,
display: "flex",
justifyContent: "center",
pointerEvents: "none",
...(titleSettings.position === "top"
? { top: "10%" }
: titleSettings.position === "center"
? { top: "50%", transform: "translateY(-50%)" }
: { bottom: "15%" }),
}}
>
<span
style={{
fontSize: titleSettings.size,
fontFamily: titleSettings.font || "思源黑体",
color: titleSettings.color || "#ffffff",
fontWeight: titleSettings.bold ? 700 : 400,
fontStyle: titleSettings.italic ? "italic" : "normal",
textShadow: [
titleSettings.shadow ? "0 2px 8px rgba(0,0,0,0.7)" : undefined,
titleSettings.stroke
? "1px 1px 0 rgba(0,0,0,0.5), -1px -1px 0 rgba(0,0,0,0.5), 1px -1px 0 rgba(0,0,0,0.5), -1px 1px 0 rgba(0,0,0,0.5)"
: undefined,
"0 1px 3px rgba(0,0,0,0.4)",
]
.filter(Boolean)
.join(", "),
maxWidth: "90%",
textAlign: "center",
lineHeight: 1.3,
wordBreak: "break-word",
}}
>
{titleSettings.title}
</span>
</div>
)}
{/* 中央播放按钮 */}
{!isPlaying && ( {!isPlaying && (
<button <button
className="xx-preview-play-btn"
onClick={handleTogglePlay} onClick={handleTogglePlay}
style={{ style={{
position: "absolute", position: "absolute",
top: "50%", top: "50%",
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
background: "rgba(0,0,0,0.5)", background: "rgba(0,0,0,0.45)",
border: "none", backdropFilter: "blur(12px)",
WebkitBackdropFilter: "blur(12px)",
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "50%", borderRadius: "50%",
width: 56, width: 52,
height: 56, height: 52,
cursor: "pointer", cursor: "pointer",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
color: "#fff", color: "#fff",
fontSize: 28, fontSize: 26,
zIndex: 10, zIndex: 10,
transition: "transform 0.2s ease, background 0.2s ease",
boxShadow: "0 4px 20px rgba(0,0,0,0.4)",
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = "translate(-50%, -50%) scale(1.08)"
e.currentTarget.style.background = "rgba(0,0,0,0.6)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = "translate(-50%, -50%) scale(1)"
e.currentTarget.style.background = "rgba(0,0,0,0.45)"
}} }}
> >
<PlayCircleOutlined /> <PlayCircleOutlined />
</button> </button>
)} )}
{/* 片段指示器 */} {/* 片段指示器 — 右上角胶囊 */}
<div <div
style={{ style={{
position: "absolute", position: "absolute",
top: 8, top: 8,
left: 8, right: 8,
background: "rgba(0,0,0,0.6)", background: "rgba(0,0,0,0.45)",
color: "#fff", backdropFilter: "blur(8px)",
fontSize: 11, WebkitBackdropFilter: "blur(8px)",
color: "rgba(255,255,255,0.9)",
fontSize: 10,
fontWeight: 500,
padding: "2px 8px", padding: "2px 8px",
borderRadius: 4, borderRadius: 999,
zIndex: 10, zIndex: 10,
border: "1px solid rgba(255,255,255,0.1)",
letterSpacing: 0.3,
}} }}
> >
{useWebCodecs ? "Canvas" : `片段 ${videoCurrentSegIdx + 1}/${segments.length}`} {`${videoCurrentSegIdx + 1} / ${segments.length}`}
</div> </div>
{/* 控制条 */} {/* 控制条 — 手机风格毛玻璃 */}
<div <div
className="xx-preview-controls"
style={{ style={{
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
@@ -436,23 +549,36 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
right: 0, right: 0,
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
gap: 8, gap: 10,
padding: "8px 12px", padding: "12px 16px 16px",
background: "linear-gradient(transparent, rgba(0,0,0,0.6))", background: "linear-gradient(transparent, rgba(0,0,0,0.7))",
backdropFilter: "blur(4px)",
WebkitBackdropFilter: "blur(4px)",
zIndex: 10, zIndex: 10,
}} }}
> >
<button <button
onClick={handleTogglePlay} onClick={handleTogglePlay}
style={{ style={{
background: "none", background: "rgba(255,255,255,0.15)",
border: "none", border: "none",
color: "#fff", color: "#fff",
fontSize: 18, fontSize: 16,
cursor: "pointer", cursor: "pointer",
padding: 4, width: 32,
height: 32,
borderRadius: "50%",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center",
flexShrink: 0,
transition: "background 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.25)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(255,255,255,0.15)"
}} }}
> >
{isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />} {isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
@@ -460,10 +586,11 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
<span <span
style={{ style={{
fontSize: 12, fontSize: 11,
color: "rgba(255,255,255,0.8)", color: "rgba(255,255,255,0.85)",
minWidth: 80, minWidth: 72,
fontVariantNumeric: "tabular-nums", fontVariantNumeric: "tabular-nums",
letterSpacing: 0.2,
}} }}
> >
{formatTime(currentTime)} / {formatTime(totalDuration)} {formatTime(currentTime)} / {formatTime(totalDuration)}
@@ -474,8 +601,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
style={{ style={{
flex: 1, flex: 1,
height: 4, height: 3,
background: "rgba(255,255,255,0.15)", background: "rgba(255,255,255,0.2)",
borderRadius: 2, borderRadius: 2,
cursor: "pointer", cursor: "pointer",
position: "relative", position: "relative",
@@ -485,7 +612,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
style={{ style={{
height: "100%", height: "100%",
width: `${progressPercent}%`, width: `${progressPercent}%`,
background: "#3b82f6", background: "#fff",
borderRadius: 2, borderRadius: 2,
transition: isDragging ? "none" : "width 0.1s linear", transition: isDragging ? "none" : "width 0.1s linear",
}} }}
@@ -499,15 +626,15 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
width: 10, width: 10,
height: 10, height: 10,
borderRadius: "50%", borderRadius: "50%",
background: "#3b82f6", background: "#fff",
border: "2px solid #fff", boxShadow: "0 0 6px rgba(255,255,255,0.5)",
opacity: isDragging ? 1 : 0, opacity: isDragging ? 1 : 0,
transition: "opacity 0.15s", transition: "opacity 0.15s",
}} }}
/> />
</div> </div>
</div> </div>
</> </div>
) )
} }
@@ -76,6 +76,14 @@ export interface GenerateStepContentProps {
onDismissError: () => void onDismissError: () => void
/* 其他 */ /* 其他 */
presetVoices: PresetVoiceItem[] presetVoices: PresetVoiceItem[]
/** 预览任务创建回调——传递给 Step6CoverSettings */
onPreviewTaskCreated?: (taskId: string) => void
/** 从预览响应中提取到 source_edit_plan_id 时的回调 */
onSourceEditPlanIdExtracted?: (planId: string) => void
/** BGM 开关 */
bgm: boolean
/** BGM 配置(来自模板) */
bgmConfig?: { enabled: boolean; music_id?: string }
} }
export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) => { export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) => {
@@ -121,8 +129,16 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onRetry, onRetry,
onDismissError, onDismissError,
presetVoices, presetVoices,
onPreviewTaskCreated,
onSourceEditPlanIdExtracted,
bgm,
bgmConfig,
} = props } = props
/* 当前模板的 segments,传给 Step2 构建 clips */
const currentTemplate = userTemplates.find((t) => t.id === selectedTemplate)
const templateSegments = currentTemplate?.segments
switch (currentStep) { switch (currentStep) {
case 1: case 1:
return ( return (
@@ -141,6 +157,8 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
onSelectedMaterialsChange={onSelectedMaterialsChange} onSelectedMaterialsChange={onSelectedMaterialsChange}
smartSelectedIds={smartSelectedIds} smartSelectedIds={smartSelectedIds}
onSmartSelectedIdsChange={onSmartSelectedIdsChange} onSmartSelectedIdsChange={onSmartSelectedIdsChange}
selectedTemplate={selectedTemplate}
templateSegments={templateSegments}
/> />
) )
case 3: case 3:
@@ -156,6 +174,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
<Step4TitleSettings <Step4TitleSettings
titleSettings={titleSettings} titleSettings={titleSettings}
onTitleSettingsChange={onTitleSettingsChange} onTitleSettingsChange={onTitleSettingsChange}
selectedTemplate={selectedTemplate}
/> />
) )
case 5: case 5:
@@ -182,6 +201,14 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
duration={duration} duration={duration}
assetIds={materialMode === "auto" ? smartSelectedIds : selectedMaterials} assetIds={materialMode === "auto" ? smartSelectedIds : selectedMaterials}
selectedTemplate={selectedTemplate} selectedTemplate={selectedTemplate}
titleSettings={titleSettings}
onPreviewTaskCreated={onPreviewTaskCreated}
onSourceEditPlanIdExtracted={onSourceEditPlanIdExtracted}
voiceMode={voiceMode}
selectedVoice={selectedVoice}
selectedClonedVoice={selectedClonedVoice}
bgm={bgm}
bgmConfig={bgmConfig}
/> />
) )
case 7: case 7:
@@ -1,38 +1,42 @@
/** /**
* 右侧预览视频面板 * 右侧预览视频面板 — 服务器渲染预览架构
* Step4+: 显示预览视频面板
* Step5: 前端实时预览 — 用原生 video 播放素材片段 + CSS 标题叠加
* *
* 架构改造:完全去除后端 FFmpeg 预览依赖 * Step4+: 显示预览面板
* - 使用 FrontendPreviewPlayer 直接播放素材片段 * Step5: 播放服务器渲染的真实视频(POST /generation/preview
* - TitleOverlay CSS 层实时响应标题样式变化
* *
* 布局:本组件提供 .xx-preview-video 容器(position: relative + overflow: hidden * 架构:
* FrontendPreviewPlayer 的内容通过 absolute 定位填充容器 * - 进入 Step4/5 时自动创建服务器预览渲染任务
* TitleOverlay 通过 absolute 定位 + z-index: 30 覆盖在最上层 * - 轮询完成后用 <video> 标签播放返回的 video_url
* - 标题样式编辑时 CSS TitleOverlay 实时叠加预览
* - 素材/配音/BGM 变更自动重新渲染
* - 标题文字/样式变更标记 stale,保留旧视频 + 显示"重新预览"按钮
*
* 点"确认生成"时走 confirm 路径,成品就是预览视频本身,100% 一致。
*/ */
import React, { useMemo, useRef, useState, useEffect } from "react" import React, { useMemo, useRef, useState, useEffect } from "react"
import { LoadingOutlined } from "@ant-design/icons" import { LoadingOutlined, ReloadOutlined, ExclamationCircleOutlined } from "@ant-design/icons"
import type { AssetItem } from "@/api/assets" import { Button } from "antd"
import type { EditingTemplate } from "@/api/editing-planner"
import type { TitleSettings } from "../types" import type { TitleSettings } from "../types"
import FrontendPreviewPlayer from "./FrontendPreviewPlayer" import { getFontFamily } from "../constants"
import type { ServerPreviewStatus } from "../hooks/useServerPreview"
interface PreviewVideoPanelProps { interface PreviewVideoPanelProps {
/** 已加载的素材列表 */ /** 服务器预览状态 */
assets: AssetItem[] previewStatus: ServerPreviewStatus
/** 当前模板 */ /** 服务器渲染视频 URL */
template: EditingTemplate | null videoUrl: string | null
/** 渲染进度 0-100 */
progress: number
/** 错误信息 */
error: string | null
/** 重新预览回调 */
onRetry: () => void
/** 视频比例 */ /** 视频比例 */
videoRatio: string videoRatio: string
/** 素材是否已加载就绪 */ /** 标题设置 — CSS 实时预览层 */
assetsReady: boolean
/** 素材是否正在加载 */
assetsLoading: boolean
/** 标题设置 — 用于 CSS 实时预览层 */
titleSettings?: TitleSettings titleSettings?: TitleSettings
/** 配音音频 URL */ /** 素材数量 */
voiceAudioUrl?: string assetCount?: number
} }
/* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */ /* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */
@@ -41,13 +45,8 @@ const ASS_TITLE_MARGIN_TOP = 60
const ASS_TITLE_MARGIN_BOTTOM = 60 const ASS_TITLE_MARGIN_BOTTOM = 60
const ASS_TITLE_MARGIN_SIDE = 40 const ASS_TITLE_MARGIN_SIDE = 40
/**
* 根据 position 计算 CSS 垂直定位
* 与后端 position_to_ass_alignment() 对齐:top→8, center→5, bottom→2
*/
function getPositionStyle(position: string): React.CSSProperties { function getPositionStyle(position: string): React.CSSProperties {
const sidePercent = (ASS_TITLE_MARGIN_SIDE / 1280) * 100 const sidePercent = (ASS_TITLE_MARGIN_SIDE / 1280) * 100
switch (position) { switch (position) {
case "bottom": case "bottom":
return { return {
@@ -75,19 +74,14 @@ function getPositionStyle(position: string): React.CSSProperties {
} }
} }
/**
* 构建 CSS 标题层的样式
* 所有渲染参数与后端 FFmpeg ASS 字幕一致
*/
function buildTitleStyle(settings: TitleSettings, containerHeight: number): React.CSSProperties { function buildTitleStyle(settings: TitleSettings, containerHeight: number): React.CSSProperties {
// 用 px 计算 fontSize,不再依赖父元素 font-size 的百分比
const fontSizePx = const fontSizePx =
containerHeight > 0 containerHeight > 0
? (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * containerHeight ? (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * containerHeight
: (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * 400 // fallback : (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * 400
const base: React.CSSProperties = { const base: React.CSSProperties = {
fontFamily: settings.font || "思源黑体", fontFamily: getFontFamily(settings.font),
fontSize: `${fontSizePx}px`, fontSize: `${fontSizePx}px`,
color: settings.color || "#ffffff", color: settings.color || "#ffffff",
fontWeight: settings.bold ? 700 : 400, fontWeight: settings.bold ? 700 : 400,
@@ -99,28 +93,16 @@ function buildTitleStyle(settings: TitleSettings, containerHeight: number): Reac
paddingLeft: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`, paddingLeft: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`,
paddingRight: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`, paddingRight: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`,
} }
if (settings.stroke) base.WebkitTextStroke = "1px #000000"
if (settings.stroke) { if (settings.shadow) base.textShadow = "2px 2px 4px rgba(0,0,0,0.8)"
base.WebkitTextStroke = "1px #000000"
}
if (settings.shadow) {
base.textShadow = "2px 2px 4px rgba(0,0,0,0.8)"
}
return base return base
} }
/** /** CSS 标题实时预览覆盖层 */
* CSS 标题预览覆盖层
* 始终渲染:有标题显示标题,无标题显示占位文本"标题预览"
* z-index: 20(在视频 z-index:1 和控制条 z-index:10 之上)
*/
const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => { const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(400) // fallback const [containerHeight, setContainerHeight] = useState(400)
// ResizeObserver 获取容器实际高度
useEffect(() => { useEffect(() => {
const el = containerRef.current const el = containerRef.current
if (!el) return if (!el) return
@@ -131,7 +113,6 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
} }
}) })
ro.observe(el) ro.observe(el)
// 初始化也读一次
const rect = el.getBoundingClientRect() const rect = el.getBoundingClientRect()
if (rect.height > 0) setContainerHeight(rect.height) if (rect.height > 0) setContainerHeight(rect.height)
return () => ro.disconnect() return () => ro.disconnect()
@@ -143,7 +124,7 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
) )
const titleStyle = useMemo( const titleStyle = useMemo(
() => buildTitleStyle(titleSettings, containerHeight), () => buildTitleStyle(titleSettings, containerHeight),
// eslint-disable-next-line react-hooks/exhaustive-deps -- 已逐字段列出 titleSettings 依赖 // eslint-disable-next-line react-hooks/exhaustive-deps
[ [
containerHeight, containerHeight,
titleSettings.font, titleSettings.font,
@@ -169,14 +150,13 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
overflow: "hidden", overflow: "hidden",
}} }}
> >
<div <div style={{ ...positionStyle, ...titleStyle, position: "absolute" }}>
style={{ {displayTitle.split(/[/]/).map((part, i) => (
...positionStyle, <span key={i}>
...titleStyle, {i > 0 && <br />}
position: "absolute", {part}
}} </span>
> ))}
{displayTitle}
</div> </div>
</div> </div>
) )
@@ -185,30 +165,47 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
/* ── 主组件 ── */ /* ── 主组件 ── */
export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
assets, previewStatus,
template, videoUrl,
progress,
error,
onRetry,
videoRatio, videoRatio,
assetsReady,
assetsLoading,
titleSettings, titleSettings,
voiceAudioUrl, assetCount,
}) => { }) => {
const videoAspectStyle = { aspectRatio: (videoRatio || "9:16").replace(":", "/") } const videoAspectStyle = { aspectRatio: (videoRatio || "9:16").replace(":", "/") }
const isLoading = previewStatus === "loading"
const isReady = previewStatus === "ready" || previewStatus === "stale"
const isFailed = previewStatus === "failed"
const isIdle = previewStatus === "idle"
const isStale = previewStatus === "stale"
return ( return (
<div className="xx-generate-preview"> <div className="xx-generate-preview">
<div className="xx-preview-header"> <div className="xx-preview-header">
<h3></h3> <h3></h3>
{assetsReady && assets.length > 0 && <span className="xx-preview-badge"></span>} {isReady && !isStale && <span className="xx-preview-badge"></span>}
{isStale && (
<span
style={{
fontSize: 12,
color: "#faad14",
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<ExclamationCircleOutlined />
</span>
)}
{isLoading && <span className="xx-preview-badge"></span>}
</div> </div>
{/* ✅ 预览容器 — 唯一的 .xx-preview-video 容器
内部所有内容(视频、控制条、标题叠加层)通过 absolute 定位填充 */}
<div className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}> <div className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}>
{/* 加载中状态 */} {/* 加载中 */}
{assetsLoading && ( {isLoading && (
<div <div
className="xx-preview-loading-center"
style={{ style={{
position: "absolute", position: "absolute",
inset: 0, inset: 0,
@@ -217,34 +214,130 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
zIndex: 5, zIndex: 5,
background: "rgba(0,0,0,0.6)",
}} }}
> >
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin /> <LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
<p style={{ marginTop: 12, color: "rgba(255,255,255,0.8)", fontSize: 14 }}> <p style={{ marginTop: 12, color: "rgba(255,255,255,0.9)", fontSize: 14 }}>
... {progress > 0 ? `...${progress}%` : "..."}
</p>
<p style={{ marginTop: 4, color: "rgba(255,255,255,0.5)", fontSize: 12 }}>
30-60
</p> </p>
</div> </div>
)} )}
{/* 前端播放器(视频 + 控制条 + 播放按钮)*/} {/* 空闲状态(尚未触发预览) */}
<FrontendPreviewPlayer {isIdle && (
assets={assets} <div
template={template} style={{
videoRatio={videoRatio} position: "absolute",
ready={assetsReady} inset: 0,
voiceAudioUrl={voiceAudioUrl} display: "flex",
/> flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.3)",
zIndex: 5,
}}
>
<p style={{ color: "rgba(255,255,255,0.7)", fontSize: 14 }}>...</p>
</div>
)}
{/* CSS 标题实时预览层 — z-index: 20,始终渲染在内容层之上 */} {/* 服务器渲染的真实视频 */}
{titleSettings && <TitleOverlay titleSettings={titleSettings} />} {isReady && videoUrl && (
<video
key={videoUrl}
src={videoUrl}
controls
autoPlay
loop
playsInline
style={{
width: "100%",
height: "100%",
objectFit: "contain",
position: "absolute",
inset: 0,
zIndex: 1,
}}
/>
)}
{/* 标题样式实时预览层(仅在有视频时叠加) */}
{isReady && titleSettings && <TitleOverlay titleSettings={titleSettings} />}
{/* stale 遮罩:配置变更提示 */}
{isStale && (
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
padding: "10px 16px",
background: "linear-gradient(transparent, rgba(0,0,0,0.85))",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
zIndex: 30,
}}
>
<span style={{ color: "rgba(255,255,255,0.9)", fontSize: 12 }}>
</span>
<Button size="small" type="primary" icon={<ReloadOutlined />} onClick={onRetry}>
</Button>
</div>
)}
{/* 错误状态 */}
{isFailed && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.7)",
zIndex: 10,
padding: 24,
}}
>
<ExclamationCircleOutlined style={{ fontSize: 40, color: "#ff4d4f" }} />
<p
style={{
marginTop: 12,
color: "rgba(255,255,255,0.9)",
fontSize: 14,
textAlign: "center",
}}
>
{error || "预览渲染失败"}
</p>
<Button
type="primary"
icon={<ReloadOutlined />}
onClick={onRetry}
style={{ marginTop: 12 }}
>
</Button>
</div>
)}
</div> </div>
{/* 素材信息 */} {/* 素材信息 */}
{assetsReady && assets.length > 0 && ( {assetCount !== undefined && assetCount > 0 && (
<div className="xx-preview-info"> <div className="xx-preview-info">
<div className="xx-preview-info-row"> <div className="xx-preview-info-row">
<span></span> <span></span>
<span>{assets.length} </span> <span>{assetCount} </span>
</div> </div>
<div className="xx-preview-info-row"> <div className="xx-preview-info-row">
<span></span> <span></span>
@@ -2,6 +2,7 @@
* Step 2 素材选择组件 * Step 2 素材选择组件
*/ */
import React from "react" import React from "react"
import type { TemplateSegment } from "@/api/templates/types"
import { useStep2Materials } from "../hooks/useStep2Materials" import { useStep2Materials } from "../hooks/useStep2Materials"
import MaterialModeTabs from "./material/MaterialModeTabs" import MaterialModeTabs from "./material/MaterialModeTabs"
import ManualMaterialList from "./material/ManualMaterialList" import ManualMaterialList from "./material/ManualMaterialList"
@@ -15,6 +16,10 @@ interface Step2MaterialSelectProps {
onSelectedMaterialsChange: (ids: string[]) => void onSelectedMaterialsChange: (ids: string[]) => void
smartSelectedIds: string[] smartSelectedIds: string[]
onSmartSelectedIdsChange: (ids: string[]) => void onSmartSelectedIdsChange: (ids: string[]) => void
/** 当前选中的模板/草稿 ID,用于自动保存 */
selectedTemplate?: string
/** 当前模板的 segments(用于构建 clips duration */
templateSegments?: TemplateSegment[]
} }
const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => { const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => {
@@ -12,6 +12,8 @@ import AiTitleGenerator from "./title/AiTitleGenerator"
interface Step4TitleSettingsProps { interface Step4TitleSettingsProps {
titleSettings: TitleSettings titleSettings: TitleSettings
onTitleSettingsChange: (settings: TitleSettings) => void onTitleSettingsChange: (settings: TitleSettings) => void
/** 当前选中的模板/草稿 ID,用于自动保存 */
selectedTemplate?: string
} }
const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => { const Step4TitleSettings: React.FC<Step4TitleSettingsProps> = (props) => {
@@ -1,7 +1,10 @@
/** /**
* Step 5 生成预览组件 * Step 5 预览设置组件
* 架构改造:移除后端预览生成,改为前端实时预览 *
* 左侧仅保留标题样式面板,视频在右侧 PreviewVideoPanel 实时播放 * 前端实时预览架构:
* - 右侧面板使用 FrontendPreviewPlayer 实时播放素材片段
* - 标题样式可实时调整,CSS 层即时叠加预览
* - 点"确认生成"时触发一次服务器渲染
*/ */
import React from "react" import React from "react"
import { PlayCircleOutlined } from "@ant-design/icons" import { PlayCircleOutlined } from "@ant-design/icons"
@@ -10,7 +13,6 @@ import type { TitleSettings } from "../types"
import TitleStylePanel from "./title/TitleStylePanel" import TitleStylePanel from "./title/TitleStylePanel"
interface Step5GeneratePreviewProps { interface Step5GeneratePreviewProps {
/* 标题样式 */
titleSettings: TitleSettings titleSettings: TitleSettings
onUpdatePosition: (position: string) => void onUpdatePosition: (position: string) => void
onUpdateFont: (font: string) => void onUpdateFont: (font: string) => void
@@ -41,9 +43,7 @@ const Step5GeneratePreview: React.FC<Step5GeneratePreviewProps> = ({
<div className="xx-form-section"> <div className="xx-form-section">
<h3>🎬 </h3> <h3>🎬 </h3>
{/* 前端实时预览提示 */}
<div <div
className="xx-preview-tip"
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -57,11 +57,10 @@ const Step5GeneratePreview: React.FC<Step5GeneratePreviewProps> = ({
> >
<PlayCircleOutlined style={{ fontSize: 18, color: "#3b82f6" }} /> <PlayCircleOutlined style={{ fontSize: 18, color: "#3b82f6" }} />
<span style={{ fontSize: 13, color: "var(--text-secondary, #666)" }}> <span style={{ fontSize: 13, color: "var(--text-secondary, #666)" }}>
</span> </span>
</div> </div>
{/* 标题样式面板 */}
<TitleStylePanel <TitleStylePanel
settings={titleSettings} settings={titleSettings}
onUpdatePosition={onUpdatePosition} onUpdatePosition={onUpdatePosition}
@@ -17,6 +17,16 @@ interface Step5VoiceSelectProps {
} }
/** 格式化时长 mm:ss */ /** 格式化时长 mm:ss */
/** 获取素材实际时长(优先顶层 durationfallback 到 metadata.duration */
const getDuration = (item: AssetItem): number => {
return item.duration ?? (item.metadata?.duration as number) ?? 0
}
/** 获取素材实际文件大小 */
const getFileSize = (item: AssetItem): number => {
return item.file_size ?? (item.metadata?.file_size as number) ?? 0
}
const formatDuration = (seconds?: number): string => { const formatDuration = (seconds?: number): string => {
if (!seconds || seconds <= 0) return "00:00" if (!seconds || seconds <= 0) return "00:00"
const m = Math.floor(seconds / 60) const m = Math.floor(seconds / 60)
@@ -89,7 +99,7 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
// 如果启用了时长校验,且配音时长不足 // 如果启用了时长校验,且配音时长不足
if (totalVideoDuration > 0) { if (totalVideoDuration > 0) {
const material = materials.find((m) => m.id === id) const material = materials.find((m) => m.id === id)
if (material && (material.duration || 0) < totalVideoDuration) { if (material && getDuration(material) < totalVideoDuration) {
setPendingVoiceId(id) setPendingVoiceId(id)
setDurationWarningOpen(true) setDurationWarningOpen(true)
return return
@@ -271,25 +281,24 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
}} }}
> >
<span style={{ display: "flex", alignItems: "center", gap: 4 }}> <span style={{ display: "flex", alignItems: "center", gap: 4 }}>
{formatDuration(item.duration)} {formatDuration(getDuration(item))}
{totalVideoDuration > 0 && {totalVideoDuration > 0 && getDuration(item) < Number(totalVideoDuration) && (
(Number(item.duration) || 0) < Number(totalVideoDuration) && ( <span
<span style={{
style={{ color: "#ff4d4f",
color: "#ff4d4f", fontSize: 11,
fontSize: 11, fontWeight: 500,
fontWeight: 500, display: "inline-flex",
display: "inline-flex", alignItems: "center",
alignItems: "center", gap: 2,
gap: 2, }}
}} >
> <WarningOutlined />
<WarningOutlined />
</span>
</span> )}
)}
</span> </span>
<span>{formatFileSize(item.file_size)}</span> <span>{formatFileSize(getFileSize(item))}</span>
</div> </div>
</div> </div>
) )
@@ -318,7 +327,9 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
return ( return (
<p> <p>
<strong>{pendingMaterial ? formatDuration(pendingMaterial.duration) : "--"}</strong> <strong>
{pendingMaterial ? formatDuration(getDuration(pendingMaterial)) : "--"}
</strong>
<strong>{formatDuration(totalVideoDuration)}</strong> <strong>{formatDuration(totalVideoDuration)}</strong>
@@ -14,6 +14,22 @@ interface Step6CoverSettingsProps {
assetIds?: string[] assetIds?: string[]
/** 当前选中的模板 ID */ /** 当前选中的模板 ID */
selectedTemplate?: string selectedTemplate?: string
/** Step4 标题设置,用于预览视频烧录标题 & 封面叠加标题 */
titleSettings?: import("../types").TitleSettings
/** 预览任务创建回调——将 task_id 暴露给父组件供 confirmGeneration 复用 */
onPreviewTaskCreated?: (taskId: string) => void
/** 从预览响应中提取到 source_edit_plan_id 时的回调 */
onSourceEditPlanIdExtracted?: (planId: string) => void
/** 配音模式 */
voiceMode?: "preset" | "custom" | "clone"
/** 选中的配音素材 ID */
selectedVoice?: string
/** 选中的克隆音色 ID */
selectedClonedVoice?: string
/** BGM 开关 */
bgm?: boolean
/** BGM 配置 */
bgmConfig?: { enabled: boolean; music_id?: string }
} }
const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => { const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
@@ -40,6 +56,14 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
duration: props.duration, duration: props.duration,
assetIds: props.assetIds, assetIds: props.assetIds,
selectedTemplate: props.selectedTemplate, selectedTemplate: props.selectedTemplate,
titleSettings: props.titleSettings,
onPreviewTaskCreated: props.onPreviewTaskCreated,
onSourceEditPlanIdExtracted: props.onSourceEditPlanIdExtracted,
voiceMode: props.voiceMode,
selectedVoice: props.selectedVoice,
selectedClonedVoice: props.selectedClonedVoice,
bgm: props.bgm,
bgmConfig: props.bgmConfig,
}) })
const handleAutoGenerate = () => { const handleAutoGenerate = () => {
@@ -2,6 +2,7 @@
* 标题预设样式网格 * 标题预设样式网格
*/ */
import React from "react" import React from "react"
import { getFontFamily } from "../../constants"
interface TitlePresetItem { interface TitlePresetItem {
key: string key: string
@@ -35,7 +36,7 @@ const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({
> >
<span <span
className="xx-title-preset-preview-text" className="xx-title-preset-preview-text"
style={{ ...p.previewStyle, ...(fontFamily ? { fontFamily } : {}) }} style={{ ...p.previewStyle, fontFamily: getFontFamily(fontFamily || "思源黑体") }}
> >
</span> </span>
+15
View File
@@ -56,6 +56,21 @@ export const FONT_OPTIONS = [
"华康俪金黑", "华康俪金黑",
] ]
/* ── 标题字体 CSS font-family 映射(中文显示名 → 浏览器可识别的字体栈) ── */
export const FONT_FAMILY_MAP: Record<string, string> = {
: '"Source Han Sans SC", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif',
: '"Source Han Serif SC", "Noto Serif SC", "Songti SC", "SimSun", serif',
: '"PingFang SC", -apple-system, "Helvetica Neue", sans-serif',
PingFang: '"PingFang SC", -apple-system, "Helvetica Neue", sans-serif',
: '"Microsoft YaHei", "PingFang SC", sans-serif',
: '"KaiTi", "STKaiti", "DFKai-SB", serif',
: '"华康俪金黑", "DFLiJinHei-W8", "Source Han Sans SC", "Microsoft YaHei", sans-serif',
}
export function getFontFamily(font: string): string {
return FONT_FAMILY_MAP[font] || FONT_FAMILY_MAP["思源黑体"]
}
/* ── 标题样式预设 ── */ /* ── 标题样式预设 ── */
export const TITLE_PRESETS = [ export const TITLE_PRESETS = [
{ {
+3
View File
@@ -2309,6 +2309,8 @@
.xx-cover-preview-box { .xx-cover-preview-box {
position: relative; position: relative;
aspect-ratio: 9 / 16; aspect-ratio: 9 / 16;
max-width: 180px;
margin: 0 auto;
background: var(--bg-tertiary); background: var(--bg-tertiary);
border-radius: var(--radius-md); border-radius: var(--radius-md);
overflow: hidden; overflow: hidden;
@@ -2523,6 +2525,7 @@
.xx-generate-right-col { .xx-generate-right-col {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center;
gap: 16px; gap: 16px;
} }
@@ -19,6 +19,17 @@ export interface UseGenerateVideoProps {
autoSubtitles: boolean autoSubtitles: boolean
bgm: boolean bgm: boolean
generateCount: number generateCount: number
/** 当前草稿 IDURL 参数 edit_plan_id,用于后端回写任务关联) */
sourceEditPlanId?: string | null
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 confirmGeneration 复用预览产物) */
previewTaskId?: string | null
/** BGM 配置(来自模板 bgm_config,受 bgm 开关控制) */
bgmConfig?: {
enabled: boolean
music_id?: string
}
/** 生成成功后的回调(用于清除持久化的 previewTaskId 等状态) */
onGenerationSuccess?: () => void
} }
/** 生成阶段 */ /** 生成阶段 */
@@ -1,87 +1,146 @@
import { useRef, useCallback } from "react" import { useRef, useCallback } from "react"
import { message } from "antd" import { message } from "antd"
import { getGenerationStatus, getGenerationTaskResults } from "@/api/template-editor" import axios from "axios"
import { getGenerationTask } from "@/api/tasks/tasks"
import { getGenerationTaskResults } from "@/api/template-editor"
import { safeExtractError } from "./errorUtils" import { safeExtractError } from "./errorUtils"
interface UseGenerationPollingOptions { interface UseGenerationPollingOptions {
templateId: string
onProgress: (progress: number) => void onProgress: (progress: number) => void
onComplete: (videos: unknown[]) => void onComplete: (videos: unknown[]) => void
onFailed: (errorMsg: string) => void onFailed: (errorMsg: string) => void
} }
/** 最大连续错误次数(仅对可重试错误),超过后终止轮询 */
const MAX_RETRYABLE_ERRORS = 10
/** 获取结果的最大重试次数 */
const MAX_RESULTS_RETRIES = 3
/** /**
* 生成状态轮询 Hook * 生成状态轮询 Hookv2 — 改用 /generation/tasks/{task_id}
* 轮询生成状态,更新进度,处理完成/失败 *
* 旧版轮询 GET /templates/{id}/editor/generation-status 依赖 plan 维度状态,
* 在编辑流程数据链路断裂时拿不到 task_id。新版直接使用 POST /generation/tasks
* 返回的 task_id 轮询任务详情,不再依赖 plan。
*
* 错误处理:
* - 4xx(尤其 404)视为不可恢复,立即 onFailed,不再重试
* - 5xx / 网络错误重试,最多连续 MAX_RETRYABLE_ERRORS 次
* - 任务完成后获取结果失败会重试 MAX_RESULTS_RETRIES 次,仍失败则 onFailed
*/ */
export const useGenerationPolling = ({ export const useGenerationPolling = ({
templateId,
onProgress, onProgress,
onComplete, onComplete,
onFailed, onFailed,
}: UseGenerationPollingOptions) => { }: UseGenerationPollingOptions) => {
const progressTimer = useRef<ReturnType<typeof setTimeout>>() const progressTimer = useRef<ReturnType<typeof setTimeout>>()
const cancelledRef = useRef(false)
const clearTimer = useCallback(() => { const clearTimer = useCallback(() => {
cancelledRef.current = true
if (progressTimer.current) { if (progressTimer.current) {
clearTimeout(progressTimer.current) clearTimeout(progressTimer.current)
progressTimer.current = undefined progressTimer.current = undefined
} }
}, []) }, [])
const startPolling = useCallback(() => { /** 任务完成后拉取结果列表,带重试 */
const poll = async () => { const fetchResultsWithRetry = useCallback(
async (taskId: string, attempt = 0): Promise<unknown[] | null> => {
try { try {
const data = await getGenerationStatus(templateId) return await getGenerationTaskResults(taskId)
} catch (err) {
if (data.plan_status === "completed") { if (cancelledRef.current) return null
onProgress(100) console.error(`[获取生成结果失败] 第 ${attempt + 1}`, err)
// 获取生成的视频结果 if (attempt < MAX_RESULTS_RETRIES - 1) {
let videos: unknown[] = [] await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1)))
if (data.generation_task_id) { return fetchResultsWithRetry(taskId, attempt + 1)
try {
videos = await getGenerationTaskResults(data.generation_task_id)
} catch (err) {
console.error("[获取生成结果失败]", err)
}
}
onComplete(videos)
message.success("视频生成完成!")
return
} }
if (data.plan_status === "failed") { return null
const dataAny = data as unknown as Record<string, unknown>
const rawMsg =
dataAny.error_message ||
dataAny.error ||
dataAny.message ||
(Array.isArray(data.clips)
? (data.clips as { status: string; error_message?: string }[]).find(
(c) => c.status === "failed",
)?.error_message
: undefined) ||
"视频生成失败,请联系管理员或重试"
const errorMsg = safeExtractError(rawMsg)
console.error("[生成失败] templateId:", templateId, "响应:", data)
onFailed(errorMsg)
message.error(errorMsg)
return
}
const clips = data.clips || []
const total = clips.length || 1
const done = (clips as { status: string }[]).filter((c) => c.status === "completed").length
onProgress(Math.round((done / total) * 100))
progressTimer.current = setTimeout(poll, 2000)
} catch (pollErr) {
console.error("[轮询出错] templateId:", templateId, pollErr)
progressTimer.current = setTimeout(poll, 3000)
} }
} },
[],
)
progressTimer.current = setTimeout(poll, 2000) const startPolling = useCallback(
}, [templateId, onProgress, onComplete, onFailed]) (taskId: string) => {
cancelledRef.current = false
let consecutiveErrors = 0
const poll = async () => {
if (cancelledRef.current) return
try {
const task = await getGenerationTask(taskId)
consecutiveErrors = 0
if (task.status === "completed") {
onProgress(100)
const videos = await fetchResultsWithRetry(taskId)
if (cancelledRef.current) return
if (videos === null) {
const errorMsg = "视频已生成,但获取结果列表失败,请稍后在任务列表查看"
console.error("[生成结果获取失败] taskId:", taskId)
onFailed(errorMsg)
message.error(errorMsg)
return
}
onComplete(videos)
message.success("视频生成完成!")
return
}
if (task.status === "failed" || task.status === "cancelled") {
const rawMsg =
task.error_info?.error_message ||
task.error_message ||
(task.status === "cancelled" ? "任务已取消" : "视频生成失败,请联系管理员或重试")
const errorMsg = safeExtractError(rawMsg)
console.error("[生成失败] taskId:", taskId, "响应:", task)
onFailed(errorMsg)
message.error(errorMsg)
return
}
// pending / waiting / running — 继续轮询
const pct = Math.max(0, Math.min(99, Math.round(Number(task.progress) || 0)))
onProgress(pct)
progressTimer.current = setTimeout(poll, 2000)
} catch (pollErr) {
if (cancelledRef.current) return
console.error("[轮询出错] taskId:", taskId, pollErr)
// 4xx 不可恢复,立即失败
const status = axios.isAxiosError(pollErr) ? pollErr.response?.status : undefined
if (status && status >= 400 && status < 500) {
const msg =
(axios.isAxiosError(pollErr) &&
(pollErr.response?.data as { detail?: string; message?: string } | undefined)
?.detail) ||
(axios.isAxiosError(pollErr) &&
(pollErr.response?.data as { detail?: string; message?: string } | undefined)
?.message) ||
`查询任务失败 (${status})`
const errorMsg = safeExtractError(msg)
onFailed(errorMsg)
message.error(errorMsg)
return
}
consecutiveErrors += 1
if (consecutiveErrors >= MAX_RETRYABLE_ERRORS) {
const errorMsg = "任务状态查询连续失败,请稍后在任务列表查看结果"
onFailed(errorMsg)
message.error(errorMsg)
return
}
progressTimer.current = setTimeout(poll, 3000)
}
}
progressTimer.current = setTimeout(poll, 1500)
},
[onProgress, onComplete, onFailed, fetchResultsWithRetry],
)
return { startPolling, clearTimer } return { startPolling, clearTimer }
} }
@@ -9,8 +9,21 @@ import { createFile } from "mp4box"
import type { Movie, Sample } from "mp4box" import type { Movie, Sample } from "mp4box"
// ── 常量 ── // ── 常量 ──
/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */
const MAX_INIT_FRAMES = 60 /**
* 规范化 mp4box 提取的 codec 字符串为 WebCodecs 兼容格式
* mp4box 返回的 codec 可能包含 mp4box 特有后缀(如 avc1.640028),
* WebCodecs 要求标准 ISO BMFF codec string(如 avc1.640028
* 大部分情况下格式一致,但需要做防御性处理
*/
function normalizeCodecString(codec: string): string {
// mp4box 有时返回带空格的 codec,去掉
const trimmed = codec.trim()
// HEVC: mp4box 可能返回 hev1.1.6.L93.B0 或 hvc1.1.6.L93.B0
// WebCodecs 接受 hev1.x.x 或 hvc1.x.x,两者都可
// H.264: mp4box 返回 avc1.640028WebCodecs 也接受
return trimmed
}
// ── MP4 Box 解析辅助函数 ── // ── MP4 Box 解析辅助函数 ──
@@ -96,7 +109,7 @@ class FrameQueue {
private frames: FrameEntry[] = [] private frames: FrameEntry[] = []
private maxSize: number private maxSize: number
constructor(maxSize = 5) { constructor(maxSize = 200) {
this.maxSize = maxSize this.maxSize = maxSize
} }
@@ -108,24 +121,36 @@ class FrameQueue {
this.frames.push(entry) this.frames.push(entry)
} }
/** 获取当前时间戳应显示的帧 */ /** 获取当前时间戳应显示的帧(二分查找,O(log n) */
getCurrentFrame(timestamp: number): VideoFrame | null { getCurrentFrame(timestamp: number): VideoFrame | null {
let best: FrameEntry | null = null if (this.frames.length === 0) return null
let bestIdx = -1
for (let i = 0; i < this.frames.length; i++) { const target = timestamp + 0.01
const f = this.frames[i]
if (f.pts <= timestamp + 0.01) { // 找到最后一个 pts <= target 的帧(右边界)
best = f let lo = 0,
bestIdx = i hi = this.frames.length - 1,
bestIdx = -1
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (this.frames[mid].pts <= target) {
bestIdx = mid
lo = mid + 1
} else {
hi = mid - 1
} }
} }
if (bestIdx < 0) return null
// 关闭并移除 bestIdx 之前的所有已播放帧
for (let i = 0; i < bestIdx; i++) { for (let i = 0; i < bestIdx; i++) {
this.frames[i].frame.close() this.frames[i].frame.close()
} }
if (bestIdx >= 0) { this.frames.splice(0, bestIdx)
this.frames = this.frames.slice(bestIdx)
} // 此时 bestIdx 对应帧已在索引 0
return best?.frame ?? null return this.frames[0]?.frame ?? null
} }
clear() { clear() {
@@ -172,6 +197,10 @@ export interface CanvasPlayerState {
duration: number duration: number
isReady: boolean isReady: boolean
isBuffering: boolean isBuffering: boolean
/** WebCodecs 解码失败时为 true,调用方应 fallback 到原生 video 播放 */
hasDecodeError: boolean
/** 解码错误信息(用于 UI 展示) */
errorMessage: string
} }
export interface CanvasPlayerControls { export interface CanvasPlayerControls {
@@ -209,21 +238,29 @@ export function useCanvasPlayer(
stroke?: boolean stroke?: boolean
shadow?: boolean shadow?: boolean
}, },
onError?: (error: Error) => void,
enabled: boolean = true,
) { ) {
const [state, setState] = useState<CanvasPlayerState>({ const [state, setState] = useState<CanvasPlayerState>({
hasSupport: isWebCodecsSupported(), hasSupport: enabled && isWebCodecsSupported(),
isPlaying: false, isPlaying: false,
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
isReady: false, isReady: false,
isBuffering: false, isBuffering: false,
hasDecodeError: false,
errorMessage: "",
}) })
// ── 内部引用 ── // ── 内部引用 ──
const decoderRef = useRef<VideoDecoder | null>(null) const decoderRef = useRef<VideoDecoder | null>(null)
const frameQueueRef = useRef(new FrameQueue(600)) const frameQueueRef = useRef(new FrameQueue(200))
/** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */ /** 每个片段持久化解码器,避免每次新建导致关键帧错误 */
const decodedSegmentsRef = useRef(new Set<number>()) const segmentDecodersRef = useRef(new Map<number, VideoDecoder>())
/** 每个片段已送入解码器的 sample 游标(用于续解码) */
const segmentSampleCursorRef = useRef(new Map<number, number>())
/** 后台补充解码是否正在运行(防重入) */
const isFeedingRef = useRef(false)
/** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */ /** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */
const decodeGenerationRef = useRef(0) const decodeGenerationRef = useRef(0)
const rafRef = useRef<number>(0) const rafRef = useRef<number>(0)
@@ -234,6 +271,8 @@ export function useCanvasPlayer(
const videoDimRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 }) const videoDimRef = useRef<{ width: number; height: number }>({ width: 0, height: 0 })
const isDestroyedRef = useRef(false) const isDestroyedRef = useRef(false)
const lastProgressUpdateRef = useRef<number>(0) const lastProgressUpdateRef = useRef<number>(0)
const onErrorRef = useRef(onError)
onErrorRef.current = onError
// 计算总时长 // 计算总时长
const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0) const totalDuration = segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0)
@@ -367,7 +406,7 @@ export function useCanvasPlayer(
globalEndTime: globalStart + (segment.endTime - segment.startTime), globalEndTime: globalStart + (segment.endTime - segment.startTime),
trackId: videoTrack.id ?? 1, trackId: videoTrack.id ?? 1,
timescale: videoTrack.timescale ?? 90000, timescale: videoTrack.timescale ?? 90000,
codec: videoTrack.codec ?? "avc1.42E01E", codec: normalizeCodecString(videoTrack.codec ?? "avc1.42E01E"),
videoWidth: videoTrack.track_width || 1280, videoWidth: videoTrack.track_width || 1280,
videoHeight: videoTrack.track_height || 720, videoHeight: videoTrack.track_height || 720,
description, description,
@@ -451,87 +490,91 @@ export function useCanvasPlayer(
[segments, extractCodecDescription], [segments, extractCodecDescription],
) )
// ── 初始化 VideoDecoder 并解码指定片段 ── // ── 解码片段的一批帧(使用持久化解码器,支持从断点续解码) ──
const decodeSegment = useCallback( /**
async (_buffer: ArrayBuffer, meta: SegmentMeta, maxFrames?: number): Promise<void> => { * @param segIdx 片段索引
if (isDestroyedRef.current) return * @param maxFrames 本次最多解码多少帧
*/
const decodeSegmentBatch = useCallback(
async (segIdx: number, maxFrames: number = 60): Promise<number> => {
if (isDestroyedRef.current) return 0
const metas = segmentMetaRef.current
const meta = metas[segIdx]
if (!meta) return 0
const buffer = segmentDataRef.current.get(meta.assetId)
if (!buffer) return 0
let decoderReady = false const gen = decodeGenerationRef.current
let decoder = segmentDecodersRef.current.get(segIdx)
let cursor = segmentSampleCursorRef.current.get(segIdx) ?? 0
const samples = meta.samples
// 配置解码器(每个片段可能需要不同的 codec/分辨率 // 如果还没有解码器,新建一个(从关键帧开始,不会报 key frame 错误
const decoder = new VideoDecoder({ if (!decoder || decoder.state === "closed") {
output: (frame: VideoFrame) => { decoder = new VideoDecoder({
// 从第一帧获取实际尺寸 output: (frame: VideoFrame) => {
if (videoDimRef.current.width === 0 || videoDimRef.current.height === 0) { if (videoDimRef.current.width === 0 || videoDimRef.current.height === 0) {
videoDimRef.current = { width: frame.codedWidth, height: frame.codedHeight } 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({
const localTime = frame.timestamp / 1_000_000 frame,
const globalTime = localTime + meta.globalStartTime pts: globalTime,
frameQueueRef.current.push({ duration: (frame.duration ?? 0) / 1_000_000,
frame, })
pts: globalTime, },
duration: (frame.duration ?? 0) / 1_000_000, error: (e: DOMException) => {
}) console.error(`[useCanvasPlayer] Segment ${segIdx} decoder error:`, e)
}, // 重置该片段的解码器和游标,允许重试
error: (e: DOMException) => { segmentDecodersRef.current.delete(segIdx)
console.error("[useCanvasPlayer] Decoder error:", e) segmentSampleCursorRef.current.set(segIdx, 0)
}, },
})
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 try {
// 标记缓冲结束,让 UI 开始渲染 await decoder.configure({
codec: meta.codec,
...(meta.description ? { description: meta.description } : {}),
})
} catch (err) {
console.error(`[useCanvasPlayer] Segment ${segIdx} configure failed:`, err)
const error = err instanceof Error ? err : new Error(String(err))
setState((s) => ({
...s,
isBuffering: false,
hasDecodeError: true,
errorMessage: `视频解码失败: ${error.message || "不支持的编解码器"}`,
}))
onErrorRef.current?.(error)
return 0
}
segmentDecodersRef.current.set(segIdx, decoder)
cursor = 0
// 标记缓冲结束
setState((s) => ({ ...s, isBuffering: false })) setState((s) => ({ ...s, isBuffering: false }))
} catch (err) {
console.error("[useCanvasPlayer] Decoder configure failed for segment:", err)
return
} }
if (!decoderReady) return if (decoder.state !== "configured") return 0
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片 // 从 cursor 继续喂 sample(流水线批量提交,不 await 单个 decode
const samplesCollected = meta.samples let decoded = 0
console.log( let si = cursor
`[useCanvasPlayer] Segment ${meta.assetId}: ${samplesCollected.length} samples to decode`, while (si < samples.length && decoded < maxFrames) {
) if (decodeGenerationRef.current !== gen || isDestroyedRef.current) break
if (samplesCollected.length === 0) { if ((decoder.state as string) === "closed") break
console.warn("[useCanvasPlayer] No samples to decode for segment", meta.assetId) // 帧队列快满时停止提交(这才是真正的背压)
return if (frameQueueRef.current.size >= 180) break
} // 解码器内部队列积压过多时短暂让出线程(阈值64,给硬件足够流水线深度)
if (decoder.decodeQueueSize > 64) {
// 送入解码器 await new Promise((r) => setTimeout(r, 5))
let decodedCount = 0
let skippedCount = 0
let decodeErrors = 0
for (const sample of samplesCollected) {
if (!sample.data || isDestroyedRef.current) {
skippedCount++
continue continue
} }
if (decoder.state === "closed") break
// 初始化阶段限制解码帧数,避免帧缓冲溢出 const sample = samples[si]
if (maxFrames && decodedCount >= maxFrames) { si++
console.log( if (!sample.data) continue
`[useCanvasPlayer] Segment ${meta.assetId}: init decode limited to ${maxFrames} frames`,
)
break
}
const chunk = new EncodedVideoChunk({ const chunk = new EncodedVideoChunk({
type: sample.is_sync ? "key" : "delta", type: sample.is_sync ? "key" : "delta",
@@ -541,85 +584,73 @@ export function useCanvasPlayer(
}) })
try { try {
await decoder.decode(chunk) // 修复:await 捕获异步错误 decoder.decode(chunk)
decodedCount++ decoded++
} catch (e) { } catch (e) {
decodeErrors++ console.warn(`[useCanvasPlayer] Segment ${segIdx} decode error:`, e)
console.warn(`[useCanvasPlayer] Decode chunk error (${decodeErrors}):`, e) break
// 连续 3 次解码失败,放弃当前片段 }
if (decodeErrors >= 3) { }
console.error("[useCanvasPlayer] Too many decode errors, aborting segment")
break cursor = si
} segmentSampleCursorRef.current.set(segIdx, cursor)
// 等待解码器输出帧(最多500ms)
if (decoded > 0 && (decoder.state as string) === "configured") {
let waited = 0
while (frameQueueRef.current.size < Math.min(decoded, 10) && waited < 500) {
await new Promise((r) => setTimeout(r, 20))
waited += 20
if (isDestroyedRef.current) break
} }
} }
console.log( console.log(
`[useCanvasPlayer] Segment ${meta.assetId}: decoded ${decodedCount}, skipped ${skippedCount}, errors ${decodeErrors}, decoder.state=${decoder.state}`, `[useCanvasPlayer] Segment ${segIdx} decoded ${decoded} frames, queue size: ${frameQueueRef.current.size}`,
) )
return decoded
// 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 个片段。 * 根据当前播放时间,确保队列中有足够缓冲
* 在渲染循环中定期调用,避免一次性解码所有片段导致环形缓冲区溢出丢帧。 * 播放循环每 200ms 调用一次
* 使用"先标记再解码"模式防止并发重复解码,失败时移除标记允许重试。
*/ */
const decodeAroundPosition = useCallback( const feedFrames = useCallback(
async (currentTime: number) => { async (currentTime: number) => {
const metas = segmentMetaRef.current if (isFeedingRef.current) return
if (!metas || metas.length === 0) return isFeedingRef.current = true
try {
const metas = segmentMetaRef.current
if (!metas || metas.length === 0) return
// 记录当前代数,seek 后代数变化则中止 // 队列帧数充足时不解码(目标:保持 >= 80 帧缓冲)
const gen = decodeGenerationRef.current if (frameQueueRef.current.size >= 80) return
let targetIdx = -1 // 找到当前播放的片段
let acc = 0 let targetIdx = 0
for (let i = 0; i < metas.length; i++) { let acc = 0
const dur = metas[i].globalEndTime - metas[i].globalStartTime for (let i = 0; i < metas.length; i++) {
if (currentTime < acc + dur) { const dur = metas[i].globalEndTime - metas[i].globalStartTime
targetIdx = i if (currentTime < acc + dur) {
break targetIdx = i
break
}
acc += dur
} }
acc += dur
}
if (targetIdx === -1) targetIdx = metas.length - 1
for ( // 依次补充:当前片段 → 下一个片段 → 再下一个
let i = Math.max(0, targetIdx - 1); for (let offset = 0; offset <= 2; offset++) {
i <= Math.min(metas.length - 1, targetIdx + 1); const idx = targetIdx + offset
i++ if (idx >= metas.length) break
) { if (frameQueueRef.current.size >= 180) break
// seek 已作废当前解码任务 await decodeSegmentBatch(idx, 60)
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 期间不更新标记 } finally {
if (decodeGenerationRef.current !== gen) return isFeedingRef.current = false
} }
}, },
[decodeSegment], [decodeSegmentBatch],
) )
// ── 标题绘制 ── // ── 标题绘制 ──
@@ -636,7 +667,6 @@ export function useCanvasPlayer(
// 按 "/" 分割为多行("/" 作为手动换行符) // 按 "/" 分割为多行("/" 作为手动换行符)
const lines = title.text.split(/[//⁄∕]/) const lines = title.text.split(/[//⁄∕]/)
console.log("[drawTitle] 原始标题:", JSON.stringify(title.text), "分割后:", lines)
const lineHeight = fontSize * 1.3 const lineHeight = fontSize * 1.3
const totalHeight = lines.length * lineHeight const totalHeight = lines.length * lineHeight
@@ -748,8 +778,10 @@ export function useCanvasPlayer(
} }
return s return s
}) })
// 按需解码当前 ±1 片段 // 后台补充帧:队列不足时自动续解码
decodeAroundPosition(currentTime) if (frameQueueRef.current.size < 80) {
void feedFrames(currentTime)
}
} }
if (currentTime >= totalDuration) { if (currentTime >= totalDuration) {
@@ -758,29 +790,46 @@ export function useCanvasPlayer(
} }
rafRef.current = requestAnimationFrame(renderFrame) rafRef.current = requestAnimationFrame(renderFrame)
}, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, decodeAroundPosition]) }, [canvasRef, totalDuration, titleSettings, drawTitle, computeDrawRect, feedFrames])
// ── 播放控制 ── // ── 播放控制 ──
const play = useCallback(async () => { const play = useCallback(async () => {
if (!state.hasSupport || isDestroyedRef.current) return if (!state.hasSupport || isDestroyedRef.current) return
// 重播场景:currentTime 已回到起点但 decodedSegmentsRef 仍有旧标记 // 重播:必须关闭旧解码器、清空队列、重置游标,从头重新解码
// 此时 FrameQueue 中旧帧已被淘汰,需清空标记让 decodeAroundPosition 重新解码 if (state.currentTime >= totalDuration - 0.1 || state.currentTime <= 0.1) {
if (state.currentTime <= 0.1 && decodedSegmentsRef.current.size > 0) {
decodeGenerationRef.current++ decodeGenerationRef.current++
decodedSegmentsRef.current.clear() // 关闭所有持久化解码器
// 同步清空帧缓冲,避免旧帧残留导致 getCurrentFrame 返回 null for (const d of segmentDecodersRef.current.values()) {
try {
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
frameQueueRef.current.clear() frameQueueRef.current.clear()
playStartOffsetRef.current = 0
setState((s) => ({ ...s, currentTime: 0 }))
// 重新初始化解码
const metas = segmentMetaRef.current
const initialDecodeCount = Math.min(metas.length, 2)
for (let i = 0; i < initialDecodeCount; i++) {
await decodeSegmentBatch(i, 60)
}
} }
setState((s) => ({ ...s, isPlaying: true })) setState((s) => ({ ...s, isPlaying: true }))
playStartRef.current = performance.now() playStartRef.current = performance.now()
playStartOffsetRef.current = state.currentTime if (state.currentTime < 0.1) {
playStartOffsetRef.current = 0
} else {
playStartOffsetRef.current = state.currentTime
}
lastProgressUpdateRef.current = 0 lastProgressUpdateRef.current = 0
rafRef.current = requestAnimationFrame(renderFrame) rafRef.current = requestAnimationFrame(renderFrame)
// 立即触发一次按需解码,不等渲染循环 200ms 节流 }, [state.hasSupport, state.currentTime, totalDuration, renderFrame, decodeSegmentBatch])
decodeAroundPosition(state.currentTime)
}, [state.hasSupport, state.currentTime, renderFrame, decodeAroundPosition])
const pause = useCallback(() => { const pause = useCallback(() => {
setState((s) => ({ ...s, isPlaying: false })) setState((s) => ({ ...s, isPlaying: false }))
@@ -790,33 +839,61 @@ export function useCanvasPlayer(
const seek = useCallback( const seek = useCallback(
async (time: number) => { async (time: number) => {
const clampedTime = Math.max(0, Math.min(time, totalDuration)) const clampedTime = Math.max(0, Math.min(time, totalDuration))
decodeGenerationRef.current++
// 关闭所有解码器、清空队列、重置游标
for (const d of segmentDecodersRef.current.values()) {
try {
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
frameQueueRef.current.clear()
setState((s) => ({ ...s, currentTime: clampedTime })) setState((s) => ({ ...s, currentTime: clampedTime }))
playStartOffsetRef.current = clampedTime playStartOffsetRef.current = clampedTime
playStartRef.current = performance.now() playStartRef.current = performance.now()
// seek 时递增解码代数,作废正在进行的异步解码 // 找到 seek 目标片段,从该片段开始解码
decodeGenerationRef.current++ const metas = segmentMetaRef.current
// 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记 let targetIdx = 0,
frameQueueRef.current.clear() acc = 0
decodedSegmentsRef.current.clear() for (let i = 0; i < metas.length; i++) {
await decodeAroundPosition(clampedTime) const dur = metas[i].globalEndTime - metas[i].globalStartTime
if (clampedTime < acc + dur) {
targetIdx = i
break
}
acc += dur
}
await decodeSegmentBatch(targetIdx, 60)
await decodeSegmentBatch(Math.min(targetIdx + 1, metas.length - 1), 60)
}, },
[totalDuration, decodeAroundPosition], [totalDuration, decodeSegmentBatch],
) )
const destroy = useCallback(() => { const destroy = useCallback(() => {
isDestroyedRef.current = true isDestroyedRef.current = true
cancelAnimationFrame(rafRef.current) cancelAnimationFrame(rafRef.current)
// 关闭所有持久化解码器
for (const d of segmentDecodersRef.current.values()) {
try {
if (d.state !== "closed") d.close()
} catch {
/* noop */
}
}
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
if (decoderRef.current && decoderRef.current.state !== "closed") { if (decoderRef.current && decoderRef.current.state !== "closed") {
decoderRef.current.close() decoderRef.current.close()
} }
// 递增代数中止进行中的异步解码,清空帧队列(clear 内部 close 所有帧)
decodeGenerationRef.current++
frameQueueRef.current.clear() frameQueueRef.current.clear()
segmentDataRef.current.clear() segmentDataRef.current.clear()
segmentMetaRef.current = [] segmentMetaRef.current = []
decodedSegmentsRef.current.clear()
}, []) }, [])
// ── 预加载下一个片段的数据 ── // ── 预加载下一个片段的数据 ──
@@ -833,6 +910,7 @@ export function useCanvasPlayer(
// ── 初始化:加载并解码所有片段 ── // ── 初始化:加载并解码所有片段 ──
useEffect(() => { useEffect(() => {
if (!enabled) return
if (!state.hasSupport || segments.length === 0) { if (!state.hasSupport || segments.length === 0) {
console.log("[useCanvasPlayer] Skip init:", { console.log("[useCanvasPlayer] Skip init:", {
hasSupport: state.hasSupport, hasSupport: state.hasSupport,
@@ -842,10 +920,24 @@ export function useCanvasPlayer(
} }
let cancelled = false let cancelled = false
console.log("[useCanvasPlayer] Init start, segments:", segments.length)
const init = async () => { const init = async () => {
setState((s) => ({ ...s, isBuffering: true })) // ✅ 关键修复:重置销毁标记,允许新的 init 周期正常工作
// destroy() 在 useEffect cleanup 中被调用,将 isDestroyedRef 设为 true
// 如果不重置,后续的 loadSegment / decodeSegment 会立即 return
isDestroyedRef.current = false
// ✅ Strict Mode 修复:init 不再递增 generation
// seek() 和 play() 仍保留 generation 递增用于中止异步解码
// 重置错误状态,避免上一轮的解码错误影响新的 init 周期
setState((s) => ({
...s,
isBuffering: true,
hasDecodeError: false,
errorMessage: "",
isReady: false,
}))
console.log("[useCanvasPlayer] Init start v2_DIAG, segments:", segments.length)
// 1. 加载所有片段数据 // 1. 加载所有片段数据
for (const seg of segments) { for (const seg of segments) {
@@ -887,32 +979,44 @@ export function useCanvasPlayer(
segmentMetaRef.current = metas segmentMetaRef.current = metas
// 3. 按需解码:初始只解码3 个片段,后续通过 decodeAroundPosition 动态加载 // 3. 初始化解码:关闭旧解码器,2 个片段各解 60 帧
// 避免一次性全量解码导致 frameQueue 环形缓冲区旧帧被丢弃引发黑屏 // 后续由 feedFrames 后台补充
decodedSegmentsRef.current.clear() for (const d of segmentDecodersRef.current.values()) {
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 { try {
await decodeSegment(buffer, meta, MAX_INIT_FRAMES) if (d.state !== "closed") d.close()
} catch (e) { } catch {
// 解码失败则移除标记,允许后续重试 /* noop */
decodedSegmentsRef.current.delete(i)
console.warn(`[useCanvasPlayer] 初始化解码片段 ${i} 失败:`, e)
} }
} }
segmentDecodersRef.current.clear()
segmentSampleCursorRef.current.clear()
frameQueueRef.current.clear()
const initGen = decodeGenerationRef.current
const initialDecodeCount = Math.min(metas.length, 2)
console.log(
`[useCanvasPlayer] Starting init decode: ${initialDecodeCount} segments, metas: ${metas.length}`,
)
for (let i = 0; i < initialDecodeCount; i++) {
if (cancelled) break
if (decodeGenerationRef.current !== initGen) break
console.log(`[DIAG_v2] Init decode segment ${i}...`)
try {
await decodeSegmentBatch(i, 60)
console.log(`[DIAG_v2] Init decode segment ${i} done`)
} catch (e) {
console.warn(`[useCanvasPlayer] 初始化解码片段 ${i} 失败:`, e)
}
if (cancelled) break
}
console.log(`[useCanvasPlayer] Init decode finished, cancelled:`, cancelled)
if (!cancelled) { if (!cancelled) {
console.log("[useCanvasPlayer] Init complete, isReady = true") console.log("[useCanvasPlayer] Init complete, isReady = true, duration:", totalDuration)
setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false })) setState((s) => ({ ...s, duration: totalDuration, isReady: true, isBuffering: false }))
} else {
console.warn("[useCanvasPlayer] Init was cancelled before completion")
} }
} }
@@ -0,0 +1,124 @@
/**
* 草稿自动保存工具 Hook
*
* 背景:后端 PUT /templates/{id}/editor 的 config 是「整体替换」语义,
* 直接发送 { config: { asset_ids } } 会把 title 等其他字段覆盖掉。
* 本 Hook 统一执行「GET 当前 config → 浅合并新字段 → PUT 回去」,
* 并用串行队列 + AbortController 保证:
* - 同一时刻只有一个保存请求在飞
* - 快速连续变化时只提交最后一次
* - 组件卸载时取消未完成请求
* - 保存失败时保留补丁,自动重试(指数退避,最多 5 次)
*
* 保存失败只 console.warn,不弹窗、不阻塞。
*/
import { useCallback, useEffect, useRef } from "react"
import { getEditPlan, updateEditPlan } from "@/api/template-editor"
type ConfigPatch = Record<string, unknown>
/** 最大自动重试次数 */
const MAX_RETRIES = 5
/** 初始重试延迟(ms),每次翻倍 */
const BASE_RETRY_DELAY = 1000
export function useDraftAutoSave(templateId?: string) {
const timerRef = useRef<ReturnType<typeof setTimeout>>()
const abortRef = useRef<AbortController | null>(null)
// 待合并的补丁队列(解决「保存进行中又来了新变化」)
const pendingPatchRef = useRef<ConfigPatch | null>(null)
const savingRef = useRef(false)
const templateIdRef = useRef(templateId)
templateIdRef.current = templateId
const flush = useCallback(async (retryCount = 0) => {
const tid = templateIdRef.current
if (!tid) return
// 已有保存在飞:把新补丁暂存,等当前请求结束后再合并一次
if (savingRef.current) return
// 快照当前补丁,但先不清空 —— 成功后才清除,失败时保留以便重试
const patchToSave = pendingPatchRef.current
if (!patchToSave) {
savingRef.current = false
return
}
savingRef.current = true
const controller = new AbortController()
abortRef.current = controller
try {
// 1. 读当前 config(拿最新,避免覆盖别人/别的步骤写入的字段)
const current = await getEditPlan(tid)
if (controller.signal.aborted) return
const merged = { ...(current.config || {}), ...patchToSave }
// 2. 写回完整合并后的 config
await updateEditPlan(tid, { config: merged }, controller.signal)
// 3. 保存成功才清除已保存的补丁
// (保存期间可能有新补丁进来,只清除我们已经保存的部分)
pendingPatchRef.current = null
} catch (err) {
const name = (err as { name?: string })?.name
if (name === "CanceledError" || name === "AbortError") {
// 组件卸载或新请求取消,不重试
return
}
console.warn("[useDraftAutoSave] 自动保存草稿失败:", err)
// 保存失败:把本次尝试保存的补丁合并回 pendingPatchRef
// (保存期间可能有新补丁,新补丁优先)
pendingPatchRef.current = {
...patchToSave,
...(pendingPatchRef.current || {}),
}
// 指数退避重试
if (retryCount < MAX_RETRIES && !controller.signal.aborted) {
const delay = BASE_RETRY_DELAY * Math.pow(2, retryCount)
timerRef.current = setTimeout(() => {
void flush(retryCount + 1)
}, delay)
}
// 超过最大重试次数后,补丁仍保留在 pendingPatchRef 中,
// 下次 scheduleSave 触发时会一起带上
} finally {
savingRef.current = false
// 保存期间又积累了新变化(且不是在重试路径中),再触发一次
if (pendingPatchRef.current && !controller.signal.aborted && retryCount === 0) {
timerRef.current = setTimeout(() => {
void flush()
}, 50)
}
}
}, [])
/**
* 调度一次自动保存(防抖)
* @param patch 要合并进 config 的局部字段
* @param delay 防抖毫秒数
*/
const scheduleSave = useCallback(
(patch: ConfigPatch, delay = 500) => {
const tid = templateIdRef.current
if (!tid) return
if (timerRef.current) clearTimeout(timerRef.current)
// 累计补丁(同一周期内多次变化合并成一次写入)
pendingPatchRef.current = { ...(pendingPatchRef.current || {}), ...patch }
timerRef.current = setTimeout(() => {
void flush()
}, delay)
},
[flush],
)
useEffect(() => {
return () => {
if (timerRef.current) clearTimeout(timerRef.current)
if (abortRef.current) abortRef.current.abort()
}
}, [])
return { scheduleSave }
}
export default useDraftAutoSave
@@ -14,6 +14,7 @@ import { useTemplateSelection } from "./useTemplateSelection"
import { useTitleCoverSync } from "./useTitleCoverSync" import { useTitleCoverSync } from "./useTitleCoverSync"
import { useVoiceState } from "./useVoiceState" import { useVoiceState } from "./useVoiceState"
import { usePlanConfigLoader } from "./usePlanConfigLoader" import { usePlanConfigLoader } from "./usePlanConfigLoader"
import { usePersistedState } from "../usePersistedState"
const DEFAULT_TITLE_SETTINGS: TitleSettings = { const DEFAULT_TITLE_SETTINGS: TitleSettings = {
aiAutoSelect: false, aiAutoSelect: false,
@@ -82,11 +83,27 @@ export interface GenerateFormState {
editPlanId: string | null editPlanId: string | null
planConfigStr: string | null planConfigStr: string | null
/**
* 传给 Worker 的 source_edit_plan_id。
* 仅使用 URL 中的 edit_plan_id(从剪辑模板编辑器跳转时携带)。
* URL 没有时传 null,后端正式生成 API 会通过 template_id+user_id 兜底查找正确的 plan。
* 注意:selectedTemplate 是模板 ID,不是 edit_plan_id,不能作为此值传递。
*/
sourceEditPlanId: string | null
/* 预览弹窗 */ /* 预览弹窗 */
previewVideo: GeneratedVideo | null previewVideo: GeneratedVideo | null
setPreviewVideo: (v: GeneratedVideo | null) => void setPreviewVideo: (v: GeneratedVideo | null) => void
previewModalOpen: boolean previewModalOpen: boolean
setPreviewModalOpen: (open: boolean) => void setPreviewModalOpen: (open: boolean) => void
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 useGenerateVideo 复用) */
previewTaskId: string | null
setPreviewTaskId: (id: string | null) => void
/** 从预览响应中提取的 source_edit_plan_id(供 fallback 路径使用) */
storedSourceEditPlanId: string | null
setStoredSourceEditPlanId: (planId: string | null) => void
} }
export const useGenerateFormState = (): GenerateFormState => { export const useGenerateFormState = (): GenerateFormState => {
@@ -100,6 +117,11 @@ export const useGenerateFormState = (): GenerateFormState => {
/* ── 模板选择 ── */ /* ── 模板选择 ── */
const { selectedTemplate, setSelectedTemplate, userTemplates } = useTemplateSelection() const { selectedTemplate, setSelectedTemplate, userTemplates } = useTemplateSelection()
/* ── source_edit_plan_id:仅取 URL 参数,无则 null 让后端兜底 ── */
// selectedTemplate 是模板 ID 而非 edit_plan_id,不能混淆;
// 后端正式生成 API 会在 source_edit_plan_id 为空时通过 template_id+user_id 自动关联。
const sourceEditPlanId = editPlanId || null
/* ── 素材 ── */ /* ── 素材 ── */
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([]) const [selectedMaterials, setSelectedMaterials] = useState<string[]>([])
const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual") const [materialMode, setMaterialMode] = useState<"manual" | "auto">("manual")
@@ -147,6 +169,30 @@ export const useGenerateFormState = (): GenerateFormState => {
const [previewVideo, setPreviewVideo] = useState<GeneratedVideo | null>(null) const [previewVideo, setPreviewVideo] = useState<GeneratedVideo | null>(null)
const [previewModalOpen, setPreviewModalOpen] = useState(false) const [previewModalOpen, setPreviewModalOpen] = useState(false)
/* ── 预览任务 IDuseStep6Cover 创建预览时写入,useGenerateVideo 复用) ── */
// 持久化到 localStoragekey 按 editPlanId/templateId 区分,刷新页面后可恢复
const previewStorageKey = editPlanId
? `preview_task_id_${editPlanId}`
: selectedTemplate
? `preview_task_id_tpl_${selectedTemplate}`
: null
const [previewTaskId, setPreviewTaskId] = usePersistedState<string | null>(
previewStorageKey,
null,
)
/* ── 从预览响应中提取的 source_edit_plan_id(供 fallback 路径使用) ── */
// 持久化到 localStorage,刷新页面后 fallback 路径仍能正确传递 source_edit_plan_id
const planIdStorageKey = editPlanId
? `source_edit_plan_id_${editPlanId}`
: selectedTemplate
? `source_edit_plan_id_tpl_${selectedTemplate}`
: null
const [storedSourceEditPlanId, setStoredSourceEditPlanId] = usePersistedState<string | null>(
planIdStorageKey,
null,
)
/* ── 从 URL / 编辑计划加载配置 ── */ /* ── 从 URL / 编辑计划加载配置 ── */
usePlanConfigLoader({ usePlanConfigLoader({
editPlanId, editPlanId,
@@ -189,10 +235,15 @@ export const useGenerateFormState = (): GenerateFormState => {
autoSubtitles, autoSubtitles,
bgm, bgm,
editPlanId, editPlanId,
sourceEditPlanId,
planConfigStr, planConfigStr,
previewVideo, previewVideo,
setPreviewVideo, setPreviewVideo,
previewModalOpen, previewModalOpen,
setPreviewModalOpen, setPreviewModalOpen,
previewTaskId,
setPreviewTaskId,
storedSourceEditPlanId,
setStoredSourceEditPlanId,
} }
} }
@@ -10,10 +10,11 @@ import type { UseGenerateVideoProps } from "./generate-video/types"
import { getGenerationPhase } from "./generate-video/phase" import { getGenerationPhase } from "./generate-video/phase"
import { useGenerationPolling } from "./generate-video/useGenerationPolling" import { useGenerationPolling } from "./generate-video/useGenerationPolling"
import { validateGenerateInputs } from "./generate-video/buildPayload" import { validateGenerateInputs } from "./generate-video/buildPayload"
import { calculateResolution } from "../utils/calculateResolution"
import { extractBackendError, translateError } from "./generate-video/errorUtils" import { extractBackendError, translateError } from "./generate-video/errorUtils"
export function useGenerateVideo(props: UseGenerateVideoProps) { export function useGenerateVideo(props: UseGenerateVideoProps) {
const { selectedTemplate } = props const { selectedTemplate, onGenerationSuccess } = props
/* ── 生成状态 ── */ /* ── 生成状态 ── */
const [generating, setGenerating] = useState(false) const [generating, setGenerating] = useState(false)
@@ -23,18 +24,22 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([]) const [generatedVideos, setGeneratedVideos] = useState<GeneratedVideo[]>([])
const handleProgress = useCallback((p: number) => setProgress(p), []) const handleProgress = useCallback((p: number) => setProgress(p), [])
const handleComplete = useCallback((videos: unknown[]) => { const handleComplete = useCallback(
setGenerating(false) (videos: unknown[]) => {
setGenerated(true) setGenerating(false)
setGeneratedVideos(videos as GeneratedVideo[]) setGenerated(true)
}, []) setGeneratedVideos(videos as GeneratedVideo[])
// 生成成功后清除持久化的预览状态,避免下次进入复用旧任务
onGenerationSuccess?.()
},
[onGenerationSuccess],
)
const handleFailed = useCallback((errorMsg: string) => { const handleFailed = useCallback((errorMsg: string) => {
setGenerating(false) setGenerating(false)
setGenerateError(errorMsg) setGenerateError(errorMsg)
}, []) }, [])
const { startPolling, clearTimer } = useGenerationPolling({ const { startPolling, clearTimer } = useGenerationPolling({
templateId: selectedTemplate,
onProgress: handleProgress, onProgress: handleProgress,
onComplete: handleComplete, onComplete: handleComplete,
onFailed: handleFailed, onFailed: handleFailed,
@@ -55,51 +60,40 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
clearTimer() clearTimer()
try { try {
// 解析分辨率 // 解析分辨率(共享工具函数)
const ratio = props.videoRatio || "9:16" const { width: outputWidth, height: outputHeight } = calculateResolution(
let outputWidth: number props.videoRatio || "9:16",
let outputHeight: number )
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 = const assetIds =
props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials props.materialMode === "auto" ? props.smartSelectedIds : props.selectedMaterials
// 直接创建正式生成任务 // 封面 URL:优先 AI 生成缩略图,兜底用户上传
await createGenerationTask({ const coverUrl = props.coverSettings?.thumbnail_url || props.coverSettings?.upload_url || ""
// 解析配音参数:voiceMode=clone 时用 selectedClonedVoice,否则用 selectedVoice
const voiceLibraryId =
props.voiceMode === "clone" ? props.selectedClonedVoice || "" : props.selectedVoice || ""
// 创建生成任务(服务器渲染)
const taskResp = await createGenerationTask({
template_id: selectedTemplate, template_id: selectedTemplate,
asset_ids: assetIds, asset_ids: assetIds,
output_width: outputWidth, output_width: outputWidth,
output_height: outputHeight, output_height: outputHeight,
cover_url: props.coverSettings?.upload_url || "", cover_url: coverUrl,
custom_title: props.titleSettings?.title || "", custom_title: props.titleSettings?.title || "",
duration: props.duration || undefined, duration: props.duration || undefined,
video_ratio: props.videoRatio, video_ratio: props.videoRatio,
// 配音:优先用 voice_library_id(配音素材库 asset),兜底 voice_ids
...(voiceLibraryId ? { voice_library_id: voiceLibraryId } : {}),
...(props.selectedVoice && !voiceLibraryId ? { voice_ids: [props.selectedVoice] } : {}),
// BGM 配置:受 bgm 开关控制,enabled=false 时也显式传覆盖模板 BGM
bgm_config: {
enabled: props.bgm !== false,
...(props.bgmConfig?.music_id ? { preset_id: props.bgmConfig.music_id } : {}),
},
...(props.sourceEditPlanId ? { source_edit_plan_id: props.sourceEditPlanId } : {}),
...(props.titleSettings?.title ...(props.titleSettings?.title
? { ? {
title_config: { title_config: {
@@ -115,8 +109,12 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
} }
: {}), : {}),
}) })
const taskId = taskResp.items?.[0]?.id
startPolling() if (!taskId) {
throw new Error("创建任务成功但未返回任务 ID,请稍后在任务列表查看")
}
startPolling(taskId)
} catch (err: unknown) { } catch (err: unknown) {
console.error("[handleGenerate] 生成失败:", err) console.error("[handleGenerate] 生成失败:", err)
setGenerating(false) setGenerating(false)
@@ -0,0 +1,63 @@
import { useState, useEffect, useCallback, useRef } from "react"
/**
* 持久化到 localStorage 的 state hook
*
* 用于在页面刷新后恢复 previewTaskId / sourceEditPlanId 等关键状态。
* 当 localStorage 不可用(SSR、隐私模式等)时自动降级为普通 useState。
* 当 key 变化时(例如切换模板),自动从新 key 重新读取并更新 state。
*/
export function usePersistedState<T>(
key: string | null | undefined,
defaultValue: T,
): [T, (value: T | ((prev: T) => T)) => void] {
const storageKey = key ? `xiaoxia_${key}` : null
// defaultValue 用 ref 持有,避免作为 useEffect 依赖导致频繁重跑
const defaultValueRef = useRef(defaultValue)
defaultValueRef.current = defaultValue
const readFromStorage = useCallback((k: string | null): T => {
if (!k) return defaultValueRef.current
try {
const stored = localStorage.getItem(k)
if (stored !== null) {
return JSON.parse(stored) as T
}
} catch (e) {
// localStorage 不可用或 JSON 解析失败,使用默认值
console.warn("[usePersistedState] 读取 localStorage 失败:", e)
}
return defaultValueRef.current
}, [])
const [state, setState] = useState<T>(() => readFromStorage(storageKey))
// key 变化时(如切换模板/草稿),从新 key 重新读取,避免状态与存储不同步
useEffect(() => {
setState(readFromStorage(storageKey))
}, [storageKey, readFromStorage])
const setPersistedState = useCallback(
(value: T | ((prev: T) => T)) => {
setState((prev) => {
const nextValue = typeof value === "function" ? (value as (prev: T) => T)(prev) : value
if (storageKey) {
try {
if (nextValue === null || nextValue === undefined || nextValue === "") {
localStorage.removeItem(storageKey)
} else {
localStorage.setItem(storageKey, JSON.stringify(nextValue))
}
} catch (e) {
// localStorage 写入失败(存储空间满/隐私模式),静默忽略
console.warn("[usePersistedState] 写入 localStorage 失败:", e)
}
}
return nextValue
})
},
[storageKey],
)
return [state, setPersistedState]
}
@@ -1,55 +1,37 @@
/** /**
* 素材片段调度器 Hook(多 video 元素方案 v2 * 素材片段调度器 Hook(多 video 元素方案 v3
* 每个片段对应一个独立 <video> 元素,全部预加载,通过 display 切换实现无缝播放 *
* 替代单 video + 切 src 方案,消除片段切换延迟 * v3 修复:
* - 所有动态状态存入 ref,tick 为稳定函数,彻底消除 RAF 闭包陷阱
* - 片段切换时先启动下一个 video 再切可见性,消除冻屏间隔
* - 进度更新 200ms 节流
*/ */
import { useState, useRef, useCallback, useEffect, useMemo } from "react" import { useState, useRef, useCallback, useEffect, useMemo } from "react"
/** 单个播放片段 */
export interface PlaybackSegment { export interface PlaybackSegment {
/** 素材 ID */
assetId: string assetId: string
/** 素材视频 URL */
videoUrl: string videoUrl: string
/** 片段在素材中的入点(秒) */
startTime: number startTime: number
/** 片段在素材中的出点(秒) */
endTime: number endTime: number
/** 片段在时间线中的顺序 */
order: number order: number
} }
/** 调度器返回 */
export interface SegmentSchedulerState { export interface SegmentSchedulerState {
/** 是否正在播放 */
isPlaying: boolean isPlaying: boolean
/** 当前播放的全局时间(秒) */
currentTime: number currentTime: number
/** 总时长(秒) */
totalDuration: number totalDuration: number
/** 当前片段索引 */
currentSegmentIndex: number currentSegmentIndex: number
/** 当前片段的本地播放时间 */
segmentLocalTime: number segmentLocalTime: number
/** 是否已播完 */
isEnded: boolean isEnded: boolean
/** 是否可以播放(至少有 1 个片段) */
canPlay: boolean canPlay: boolean
/** 播放 */
play: () => void play: () => void
/** 暂停 */
pause: () => void pause: () => void
/** 切换播放/暂停 */
togglePlayPause: () => void togglePlayPause: () => void
/** 跳转到全局时间 */
seekTo: (time: number) => void seekTo: (time: number) => void
/** 每个片段对应的 video 元素 ref 数组 */
videoRefs: React.MutableRefObject<(HTMLVideoElement | null)[]> videoRefs: React.MutableRefObject<(HTMLVideoElement | null)[]>
} }
/**
* 根据全局时间定位对应的片段和本地时间
*/
function findSegmentAtTime( function findSegmentAtTime(
segments: PlaybackSegment[], segments: PlaybackSegment[],
globalTime: number, globalTime: number,
@@ -66,9 +48,6 @@ function findSegmentAtTime(
return { index: segments.length - 1, localTime: segments[segments.length - 1].endTime } return { index: segments.length - 1, localTime: segments[segments.length - 1].endTime }
} }
/**
* 计算每个片段的全局起始时间
*/
function buildTimeline(segments: PlaybackSegment[]): number[] { function buildTimeline(segments: PlaybackSegment[]): number[] {
const starts: number[] = [] const starts: number[] = []
let acc = 0 let acc = 0
@@ -79,224 +58,242 @@ function buildTimeline(segments: PlaybackSegment[]): number[] {
return starts return starts
} }
/**
* useSegmentScheduler — 多 video 元素版素材片段调度器
*
* 核心改变:
* - 每个片段对应一个独立 <video> 元素(由组件渲染,ref 传入)
* - 所有 video 在挂载时即设置 src + preload="auto",浏览器自动预加载
* - 切换片段仅改 currentSegmentIndex + display,无需重新 load
* - 实现无缝切换,无加载延迟
*/
export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedulerState { export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedulerState {
/** 每个片段对应的 video 元素 ref(由组件 JSX 渲染并绑定) */
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]) const videoRefs = useRef<(HTMLVideoElement | null)[]>([])
const [isPlaying, setIsPlaying] = useState(false) const [isPlaying, setIsPlaying] = useState(false)
const [currentTime, setCurrentTime] = useState(0) const [currentTime, setCurrentTime] = useState(0)
const [currentSegmentIndex, setCurrentSegmentIndex] = useState(0) const [currentSegmentIndex, setCurrentSegmentIndex] = useState(0)
const [isEnded, setIsEnded] = useState(false) const [isEnded, setIsEnded] = useState(false)
const rafRef = useRef<number>(0) const rafRef = useRef(0)
const isSeekingRef = useRef(false) const isSeekingRef = useRef(false)
const lastTimeUpdateRef = useRef(0)
// 计算时间线 // 所有动态值存入 ref,tick 始终读取最新值,不依赖闭包
const timelineStarts = useMemo(() => buildTimeline(segments), [segments]) const segIdxRef = useRef(0)
const totalDuration = useMemo( const segmentsRef = useRef(segments)
const timelineStartsData = useMemo(() => buildTimeline(segments), [segments])
const totalDurationData = useMemo(
() => segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0), () => segments.reduce((sum, seg) => sum + (seg.endTime - seg.startTime), 0),
[segments], [segments],
) )
const timelineStartsRef = useRef(timelineStartsData)
const totalDurationRef = useRef(totalDurationData)
const isPlayingRef = useRef(false)
segmentsRef.current = segments
timelineStartsRef.current = timelineStartsData
totalDurationRef.current = totalDurationData
const canPlay = segments.length > 0 const canPlay = segments.length > 0
// 当前片段信息 useEffect(() => {
const currentSegment = segments[currentSegmentIndex] || null segIdxRef.current = currentSegmentIndex
const segmentLocalTime = currentSegment }, [currentSegmentIndex])
? currentTime - (timelineStarts[currentSegmentIndex] || 0) + currentSegment.startTime
: 0 useEffect(() => {
isPlayingRef.current = isPlaying
}, [isPlaying])
const waitForReady = useCallback((video: HTMLVideoElement, timeout = 3000): Promise<void> => {
if (video.readyState >= 3) return Promise.resolve()
return new Promise((resolve) => {
const onCanPlay = () => {
video.removeEventListener("canplay", onCanPlay)
clearTimeout(timer)
resolve()
}
const timer = setTimeout(() => {
video.removeEventListener("canplay", onCanPlay)
resolve()
}, timeout)
video.addEventListener("canplay", onCanPlay)
})
}, [])
/**
* 切换到指定片段
* 不改变 src(video 已在 JSX 中设置),仅 seek + 等待可播
*/
const switchToSegment = useCallback( const switchToSegment = useCallback(
(index: number, seekToLocalTime?: number): Promise<void> => { async (index: number, seekToLocalTime?: number) => {
return new Promise((resolve) => { const segs = segmentsRef.current
// 暂停当前视频 const video = videoRefs.current[index]
const prevVideo = videoRefs.current[currentSegmentIndex] if (!video || index >= segs.length) return
if (prevVideo) prevVideo.pause()
const video = videoRefs.current[index] const seg = segs[index]
if (!video || index >= segments.length) { const localTime = seekToLocalTime ?? seg.startTime
resolve() const oldIdx = segIdxRef.current
return const oldVideo = videoRefs.current[oldIdx]
}
const seg = segments[index] if (oldVideo && oldVideo !== video) oldVideo.pause()
const localTime = seekToLocalTime ?? seg.startTime
// 设置播放位置 if (!video.src && seg.videoUrl) {
video.src = seg.videoUrl
video.load()
}
if (Math.abs(video.currentTime - localTime) > 0.05) {
video.currentTime = localTime video.currentTime = localTime
}
// 如果已有足够帧数据,直接 resolve segIdxRef.current = index
if (video.readyState >= 2) { setCurrentSegmentIndex(index)
setCurrentSegmentIndex(index)
resolve()
return
}
// 等待 canplay 事件 await waitForReady(video)
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], [waitForReady],
) )
/** 播放循环 — 检测片段边界并切换 */ // 稳定的 tick 函数,空依赖,所有值从 ref 读取
const tick = useCallback(() => { const tick = useCallback(() => {
const video = videoRefs.current[currentSegmentIndex] const segs = segmentsRef.current
const idx = segIdxRef.current
const video = videoRefs.current[idx]
if (!video || isSeekingRef.current) { if (!video || isSeekingRef.current) {
rafRef.current = requestAnimationFrame(tick) rafRef.current = requestAnimationFrame(tick)
return return
} }
const seg = segments[currentSegmentIndex] const seg = segs[idx]
if (!seg) return if (!seg) return
// 检查是否到达出点(容差 0.15s // 预加载下一个片段
if (video.currentTime >= seg.endTime - 0.15) { const nextIndex = idx + 1
video.pause() if (nextIndex < segs.length) {
const nextIndex = currentSegmentIndex + 1 const nextVideo = videoRefs.current[nextIndex]
if (nextIndex < segments.length) { if (nextVideo) {
switchToSegment(nextIndex).then(() => { const timeToEnd = seg.endTime - video.currentTime
setIsPlaying(true) if (timeToEnd <= 2 && nextVideo.readyState < 3) {
rafRef.current = requestAnimationFrame(tick) const nextSeg = segs[nextIndex]
const nextVideo = videoRefs.current[nextIndex] if (Math.abs(nextVideo.currentTime - nextSeg.startTime) > 0.5) {
if (nextVideo) { nextVideo.currentTime = nextSeg.startTime
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 },
)
}
} }
}) }
}
}
// 检测片段边界
if (video.currentTime >= seg.endTime - 0.1) {
if (nextIndex < segs.length) {
const nextVideo = videoRefs.current[nextIndex]
const nextSeg = segs[nextIndex]
const accumulatedTime = const accumulatedTime =
(timelineStarts[currentSegmentIndex] || 0) + (seg.endTime - seg.startTime) (timelineStartsRef.current[idx] || 0) + (seg.endTime - seg.startTime)
if (nextVideo) {
if (Math.abs(nextVideo.currentTime - nextSeg.startTime) > 0.1) {
nextVideo.currentTime = nextSeg.startTime
}
// 先启动下一个视频(muted,可安全同时播放)
nextVideo
.play()
.catch((e) => console.warn("[useSegmentScheduler] next segment play failed:", e))
}
// 立即切换可见性
segIdxRef.current = nextIndex
setCurrentSegmentIndex(nextIndex)
setCurrentTime(accumulatedTime) setCurrentTime(accumulatedTime)
lastTimeUpdateRef.current = 0
setIsPlaying(true)
// 下一帧暂停旧视频(让新视频先渲染,避免冻屏)
const oldVideo = video
requestAnimationFrame(() => {
oldVideo.pause()
})
rafRef.current = requestAnimationFrame(tick)
return
} else { } else {
video.pause()
setIsPlaying(false) setIsPlaying(false)
setIsEnded(true) setIsEnded(true)
setCurrentTime(totalDuration) setCurrentTime(totalDurationRef.current)
return return
} }
} else { }
const globalTime =
(timelineStarts[currentSegmentIndex] || 0) + (video.currentTime - seg.startTime) const globalTime = (timelineStartsRef.current[idx] || 0) + (video.currentTime - seg.startTime)
setCurrentTime(Math.max(0, Math.min(globalTime, totalDuration))) const now = performance.now()
if (now - lastTimeUpdateRef.current >= 200) {
lastTimeUpdateRef.current = now
setCurrentTime(Math.max(0, Math.min(globalTime, totalDurationRef.current)))
} }
rafRef.current = requestAnimationFrame(tick) rafRef.current = requestAnimationFrame(tick)
}, [segments, currentSegmentIndex, timelineStarts, totalDuration, switchToSegment]) }, [])
/** 播放 */
const play = useCallback(async () => { const play = useCallback(async () => {
if (!canPlay) return if (!canPlay) return
setIsEnded(false) setIsEnded(false)
const idx = segIdxRef.current
// 确保第一段可播放 const video = videoRefs.current[idx]
const firstVideo = videoRefs.current[0]
if (firstVideo && currentSegmentIndex === 0 && firstVideo.readyState < 2) {
await switchToSegment(0)
}
const video = videoRefs.current[currentSegmentIndex]
if (!video) return if (!video) return
try { if (idx === 0 && video.readyState < 2) {
const playPromise = video.play() if (!video.src && segmentsRef.current[0]?.videoUrl) {
if (playPromise !== undefined) { video.src = segmentsRef.current[0].videoUrl
await playPromise video.load()
} }
await waitForReady(video)
}
try {
await video.play()
setIsPlaying(true) setIsPlaying(true)
cancelAnimationFrame(rafRef.current)
rafRef.current = requestAnimationFrame(tick) rafRef.current = requestAnimationFrame(tick)
} catch (err) { } catch (err) {
console.warn("[useSegmentScheduler] 播放失败:", err) console.warn("[useSegmentScheduler] 播放失败:", err)
} }
}, [canPlay, switchToSegment, tick, currentSegmentIndex]) }, [canPlay, waitForReady, tick])
/** 暂停 */
const pause = useCallback(() => { const pause = useCallback(() => {
const video = videoRefs.current[currentSegmentIndex] const video = videoRefs.current[segIdxRef.current]
if (video) video.pause() if (video) video.pause()
setIsPlaying(false) setIsPlaying(false)
cancelAnimationFrame(rafRef.current) cancelAnimationFrame(rafRef.current)
}, [currentSegmentIndex]) }, [])
/** 切换播放/暂停 */
const togglePlayPause = useCallback(() => { const togglePlayPause = useCallback(() => {
if (isPlaying) { if (isPlayingRef.current) {
pause() pause()
} else { } else {
if (isEnded) { if (isEnded) {
// 播放结束后再次播放,从头开始
setIsEnded(false) setIsEnded(false)
switchToSegment(0, segments[0]?.startTime).then(() => { lastTimeUpdateRef.current = 0
const video = videoRefs.current[0] const firstVideo = videoRefs.current[0]
if (video) { if (firstVideo) {
video.play().catch((e) => console.warn("[useSegmentScheduler] restart play failed:", e)) videoRefs.current.forEach((v, i) => {
setIsPlaying(true) if (v && i !== 0) v.pause()
setCurrentTime(0) })
rafRef.current = requestAnimationFrame(tick) firstVideo.currentTime = segmentsRef.current[0]?.startTime || 0
} segIdxRef.current = 0
}) setCurrentSegmentIndex(0)
setCurrentTime(0)
firstVideo
.play()
.then(() => {
setIsPlaying(true)
cancelAnimationFrame(rafRef.current)
rafRef.current = requestAnimationFrame(tick)
})
.catch((e) => console.warn("[useSegmentScheduler] restart failed:", e))
}
} else { } else {
play() play()
} }
} }
}, [isPlaying, isEnded, pause, play, switchToSegment, segments, tick]) }, [isEnded, pause, play, tick])
/** 跳转到指定全局时间 */
const seekTo = useCallback( const seekTo = useCallback(
async (time: number) => { async (time: number) => {
if (!canPlay) return if (!canPlay) return
const clampedTime = Math.max(0, Math.min(time, totalDurationRef.current))
const clampedTime = Math.max(0, Math.min(time, totalDuration)) const { index, localTime } = findSegmentAtTime(segmentsRef.current, clampedTime)
const { index, localTime } = findSegmentAtTime(segments, clampedTime)
isSeekingRef.current = true isSeekingRef.current = true
cancelAnimationFrame(rafRef.current)
if (index !== currentSegmentIndex) { if (index !== segIdxRef.current) {
await switchToSegment(index, localTime) await switchToSegment(index, localTime)
} else { } else {
const video = videoRefs.current[index] const video = videoRefs.current[index]
@@ -305,48 +302,54 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
setCurrentTime(clampedTime) setCurrentTime(clampedTime)
setIsEnded(false) setIsEnded(false)
lastTimeUpdateRef.current = 0
if (isPlayingRef.current) {
const video = videoRefs.current[index]
if (video) {
video.play().catch(() => {})
}
rafRef.current = requestAnimationFrame(tick)
}
setTimeout(() => { setTimeout(() => {
isSeekingRef.current = false isSeekingRef.current = false
}, 200) }, 200)
}, },
[canPlay, totalDuration, segments, currentSegmentIndex, switchToSegment], [canPlay, switchToSegment, tick],
) )
// 确保 videoRefs 数组长度与 segments 一致 + 强制预加载
useEffect(() => { useEffect(() => {
videoRefs.current = videoRefs.current.slice(0, segments.length) videoRefs.current = videoRefs.current.slice(0, segments.length)
while (videoRefs.current.length < segments.length) { while (videoRefs.current.length < segments.length) {
videoRefs.current.push(null) videoRefs.current.push(null)
} }
// 强制预加载:所有 video 元素挂载后,调用 load() 确保浏览器真正开始加载数据
videoRefs.current.forEach((video) => {
if (video) {
video.load()
}
})
}, [segments]) }, [segments])
// 组件卸载时清理
useEffect(() => { useEffect(() => {
return () => { return () => {
cancelAnimationFrame(rafRef.current) cancelAnimationFrame(rafRef.current)
} }
}, []) }, [])
// 片段列表变化时重置
useEffect(() => { useEffect(() => {
cancelAnimationFrame(rafRef.current) cancelAnimationFrame(rafRef.current)
segIdxRef.current = 0
setIsPlaying(false) setIsPlaying(false)
setCurrentTime(0) setCurrentTime(0)
setCurrentSegmentIndex(0) setCurrentSegmentIndex(0)
setIsEnded(false) setIsEnded(false)
}, [segments]) }, [segments])
const currentSegment = segments[currentSegmentIndex] || null
const segmentLocalTime = currentSegment
? currentTime - (timelineStartsRef.current[currentSegmentIndex] || 0) + currentSegment.startTime
: 0
return { return {
isPlaying, isPlaying,
currentTime, currentTime,
totalDuration, totalDuration: totalDurationData,
currentSegmentIndex, currentSegmentIndex,
segmentLocalTime, segmentLocalTime,
isEnded, isEnded,
@@ -0,0 +1,294 @@
/**
* 服务器渲染预览 Hook
*
* 核心职责:
* 1. 调用 POST /generation/preview 创建服务器预览渲染任务
* 2. 轮询 GET /generation/preview/{task_id} 直到完成
* 3. 返回服务器渲染的真实视频 URL(供 <video> 标签播放)
* 4. 检测配置变更,标记预览失效(stale)或自动重新渲染
* 5. 网络错误自动重试 2 次
*
* 状态机:
* idle → loading → ready → stale (config changed)
* ↘ failed → idle (retry)
*/
import { useState, useCallback, useRef, useEffect } from "react"
import { createPreview, getPreviewStatus } from "@/api/generation/preview"
import type { CreatePreviewRequest } from "@/api/generation/types"
export type ServerPreviewStatus = "idle" | "loading" | "ready" | "stale" | "failed"
interface UseServerPreviewOptions {
/** 是否启用预览(Step4+ 且有素材和模板时) */
enabled: boolean
/** 构建预览请求参数(每次 render 调用,获取最新配置) */
buildRequest: () => CreatePreviewRequest
/** 预览任务创建成功回调 */
onPreviewTaskCreated?: (taskId: string, sourceEditPlanId?: string) => void
}
interface UseServerPreviewReturn {
status: ServerPreviewStatus
videoUrl: string | null
error: string | null
/** 进度 0-100 */
progress: number
/** 手动触发预览创建("重新预览"按钮或标题变更后手动刷新) */
triggerPreview: () => void
/** 当前预览任务 ID */
taskId: string | null
}
const POLL_INTERVAL = 2000
const POLL_TIMEOUT = 120_000
const MAX_NETWORK_RETRIES = 2
/**
* 对配置参数做指纹,用于检测配置是否变化
*/
function buildFingerprint(req: CreatePreviewRequest): string {
return JSON.stringify({
t: req.template_id,
a: [...req.asset_ids].sort(),
d: req.duration,
r: req.video_ratio,
v: req.voice_library_id,
b: req.bgm_config,
title: req.title_config,
})
}
export function useServerPreview({
enabled,
buildRequest,
onPreviewTaskCreated,
}: UseServerPreviewOptions): UseServerPreviewReturn {
const [status, setStatus] = useState<ServerPreviewStatus>("idle")
const [videoUrl, setVideoUrl] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
const [taskId, setTaskId] = useState<string | null>(null)
const pollTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const timeoutTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const requestSeqRef = useRef(0)
const renderedFingerprintRef = useRef<string>("")
const mountedRef = useRef(true)
const networkRetriesRef = useRef(0)
// 始终持有最新的 buildRequest 和回调
const buildRequestRef = useRef(buildRequest)
buildRequestRef.current = buildRequest
const onCreatedRef = useRef(onPreviewTaskCreated)
onCreatedRef.current = onPreviewTaskCreated
/* ── 清理 ── */
const clearTimers = useCallback(() => {
if (pollTimerRef.current) {
clearTimeout(pollTimerRef.current)
pollTimerRef.current = null
}
if (timeoutTimerRef.current) {
clearTimeout(timeoutTimerRef.current)
timeoutTimerRef.current = null
}
}, [])
useEffect(() => {
mountedRef.current = true
return () => {
mountedRef.current = false
clearTimers()
}
}, [clearTimers])
/* ── 创建预览 + 轮询 ── */
const createAndPoll = useCallback(
async (request: CreatePreviewRequest, seq: number) => {
setStatus("loading")
setProgress(0)
setError(null)
networkRetriesRef.current = 0
try {
const resp = await createPreview(request)
if (seq !== requestSeqRef.current || !mountedRef.current) return
setTaskId(resp.task_id)
onCreatedRef.current?.(resp.task_id, resp.source_edit_plan_id)
let completed = false
// 超时保护
timeoutTimerRef.current = setTimeout(() => {
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
completed = true
clearTimers()
setStatus("failed")
setError("预览渲染超时(120秒),请重试")
}, POLL_TIMEOUT)
const poll = async () => {
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
try {
const st = await getPreviewStatus(resp.task_id)
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
if (st.status === "completed" && st.video_url) {
completed = true
clearTimers()
renderedFingerprintRef.current = buildFingerprint(request)
setVideoUrl(st.video_url)
setProgress(100)
setStatus("ready")
setError(null)
return
}
if (st.status === "failed" || st.status === "cancelled") {
completed = true
clearTimers()
setStatus("failed")
setError(
st.status === "cancelled"
? "预览任务已取消"
: st.error_message || "预览渲染失败,请重试",
)
return
}
// pending / generating
if (typeof st.progress === "number") setProgress(st.progress)
pollTimerRef.current = setTimeout(poll, POLL_INTERVAL)
} catch (pollErr) {
if (completed || seq !== requestSeqRef.current || !mountedRef.current) return
if (networkRetriesRef.current < MAX_NETWORK_RETRIES) {
networkRetriesRef.current += 1
console.warn(
`[ServerPreview] 轮询网络错误,第 ${networkRetriesRef.current} 次重试`,
pollErr,
)
pollTimerRef.current = setTimeout(poll, POLL_INTERVAL * 2)
} else {
completed = true
clearTimers()
setStatus("failed")
setError("网络错误,无法获取预览状态,请重试")
}
}
}
poll()
} catch (createErr) {
if (seq !== requestSeqRef.current || !mountedRef.current) return
console.error("[ServerPreview] 创建预览任务失败:", createErr)
const isNetworkError =
!!(createErr as { request?: unknown })?.request ||
(createErr as { code?: string })?.code === "ERR_NETWORK"
if (isNetworkError && networkRetriesRef.current < MAX_NETWORK_RETRIES) {
networkRetriesRef.current += 1
console.warn(`[ServerPreview] 创建任务网络错误,第 ${networkRetriesRef.current} 次重试`)
setTimeout(() => {
if (seq === requestSeqRef.current && mountedRef.current) {
createAndPoll(request, seq)
}
}, POLL_INTERVAL * 2)
return
}
const errData = (
createErr as { response?: { data?: { detail?: string; message?: string } } }
)?.response?.data
setStatus("failed")
setError(errData?.detail || errData?.message || "预览任务创建失败,请重试")
}
},
[clearTimers],
)
/* ── 手动触发预览 ── */
const triggerPreview = useCallback(() => {
if (!enabled) return
const request = buildRequestRef.current()
if (!request.template_id || request.asset_ids.length === 0) return
clearTimers()
const seq = ++requestSeqRef.current
setVideoUrl(null)
setTaskId(null)
createAndPoll(request, seq)
}, [enabled, clearTimers, createAndPoll])
/* ── 自动触发 + 配置变更检测 ── */
// 每次 render 都检查最新配置 fingerprint,与已渲染的 fingerprint 比较
const request = enabled ? buildRequest() : null
const currentFingerprint = request
? request.template_id && request.asset_ids.length > 0
? buildFingerprint(request)
: ""
: ""
// 首次进入自动触发
const didInitRef = useRef(false)
useEffect(() => {
if (!enabled || !currentFingerprint) {
didInitRef.current = false
// 禁用时取消进行中的轮询,避免回到前序步骤后仍在后台轮询
requestSeqRef.current += 1
clearTimers()
return
}
if (!didInitRef.current) {
didInitRef.current = true
renderedFingerprintRef.current = currentFingerprint
triggerPreview()
}
}, [enabled, currentFingerprint, triggerPreview, clearTimers])
// 配置变更检测:素材/配音/BGM 等变化 → 自动重渲染;标题样式变化 → 标记 stale
const prevFingerprintRef = useRef(currentFingerprint)
useEffect(() => {
if (!enabled || !currentFingerprint) return
const prev = prevFingerprintRef.current
prevFingerprintRef.current = currentFingerprint
if (!prev || prev === currentFingerprint) return
if (currentFingerprint === renderedFingerprintRef.current) return
// 配置已变更
// 判断是标题样式变更还是素材/配音/BGM 变更
const prevParsed = JSON.parse(prev) as Record<string, unknown>
const currParsed = JSON.parse(currentFingerprint) as Record<string, unknown>
const nonTitleChanged =
prevParsed.t !== currParsed.t ||
prevParsed.a !== currParsed.a ||
prevParsed.d !== currParsed.d ||
prevParsed.r !== currParsed.r ||
prevParsed.v !== currParsed.v ||
JSON.stringify(prevParsed.b) !== JSON.stringify(currParsed.b)
if (nonTitleChanged) {
// 素材/配音/BGM/模板等变化 → 自动重新渲染
renderedFingerprintRef.current = currentFingerprint
triggerPreview()
} else {
// 仅标题文字/样式变化 → 标记 stale,不自动重渲染(避免频繁请求)
// 实时预览由 CSS TitleOverlay 提供
setStatus((s) => (s === "ready" ? "stale" : s))
}
}, [enabled, currentFingerprint, triggerPreview])
return {
status,
videoUrl,
error,
progress,
triggerPreview,
taskId,
}
}
export default useServerPreview
@@ -3,9 +3,14 @@
* 组合素材库加载 + 智能匹配两个子 Hook * 组合素材库加载 + 智能匹配两个子 Hook
*/ */
import { useCallback, useEffect, useRef } from "react" import { useCallback, useEffect, useRef } from "react"
import type { AssetItem } from "@/api/assets"
import type { TemplateSegment } from "@/api/templates/types"
import { updateEditPlanClips } from "@/api/template-editor"
import { formatDuration } from "../utils/formatDuration" import { formatDuration } from "../utils/formatDuration"
import { buildClipsFromAssets } from "../utils/buildClipsFromAssets"
import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary" import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary"
import { useSmartMatch } from "./step2-materials/useSmartMatch" import { useSmartMatch } from "./step2-materials/useSmartMatch"
import { useDraftAutoSave } from "./useDraftAutoSave"
interface UseStep2MaterialsProps { interface UseStep2MaterialsProps {
materialMode: "manual" | "auto" materialMode: "manual" | "auto"
@@ -14,6 +19,10 @@ interface UseStep2MaterialsProps {
onSelectedMaterialsChange: (ids: string[]) => void onSelectedMaterialsChange: (ids: string[]) => void
smartSelectedIds: string[] smartSelectedIds: string[]
onSmartSelectedIdsChange: (ids: string[]) => void onSmartSelectedIdsChange: (ids: string[]) => void
/** 当前选中的模板/草稿 ID,用于自动保存 */
selectedTemplate?: string
/** 当前模板的 segments(用于构建 clips duration */
templateSegments?: TemplateSegment[]
} }
export function useStep2Materials({ export function useStep2Materials({
@@ -23,6 +32,8 @@ export function useStep2Materials({
onSelectedMaterialsChange, onSelectedMaterialsChange,
smartSelectedIds, smartSelectedIds,
onSmartSelectedIdsChange, onSmartSelectedIdsChange,
selectedTemplate,
templateSegments,
}: UseStep2MaterialsProps) { }: UseStep2MaterialsProps) {
const { libraries, selectedLibraryId, setSelectedLibraryId, materials, materialsLoading } = const { libraries, selectedLibraryId, setSelectedLibraryId, materials, materialsLoading } =
useMaterialLibrary() useMaterialLibrary()
@@ -53,6 +64,69 @@ export function useStep2Materials({
handleSmartMatch() handleSmartMatch()
}, [selectedLibraryId, materialMode, materialsLoading, materials.items, handleSmartMatch]) }, [selectedLibraryId, materialMode, materialsLoading, materials.items, handleSmartMatch])
/* ── Step2 选择素材后自动保存草稿 asset_ids(防抖 500ms,失败静默) ── */
const { scheduleSave } = useDraftAutoSave(selectedTemplate)
useEffect(() => {
if (!selectedTemplate) return
const ids = materialMode === "auto" ? smartSelectedIds : selectedMaterials
scheduleSave({ asset_ids: ids }, 500)
}, [selectedTemplate, materialMode, selectedMaterials, smartSelectedIds, scheduleSave])
/* ── Step2 选择素材后同步写入 edit_plan_clips(防抖 800ms,失败静默) ── */
const clipsTimerRef = useRef<ReturnType<typeof setTimeout>>()
const clipsAbortRef = useRef<AbortController | null>(null)
const templateSegmentsRef = useRef(templateSegments)
templateSegmentsRef.current = templateSegments
const selectedTemplateRef = useRef(selectedTemplate)
selectedTemplateRef.current = selectedTemplate
const materialsRef = useRef(materials)
materialsRef.current = materials
const smartMatchedRef = useRef<AssetItem[]>(smartMatch.smartMatchedResults)
smartMatchedRef.current = smartMatch.smartMatchedResults
useEffect(() => {
const tid = selectedTemplateRef.current
if (!tid) return
const ids = materialMode === "auto" ? smartSelectedIds : selectedMaterials
if (!ids.length) return
if (clipsTimerRef.current) clearTimeout(clipsTimerRef.current)
clipsTimerRef.current = setTimeout(async () => {
// 取消上一次未完成的请求
if (clipsAbortRef.current) clipsAbortRef.current.abort()
const controller = new AbortController()
clipsAbortRef.current = controller
const clips = buildClipsFromAssets({
selectedIds: ids,
materials: materialsRef.current.items,
smartMatchedAssets: smartMatchedRef.current,
templateSegments: templateSegmentsRef.current || [],
})
try {
await updateEditPlanClips(tid, clips, controller.signal)
} catch (err) {
const name = (err as { name?: string })?.name
if (name !== "CanceledError" && name !== "AbortError") {
console.warn("[useStep2Materials] 写入 clips 失败:", err)
}
}
}, 800)
return () => {
if (clipsTimerRef.current) clearTimeout(clipsTimerRef.current)
}
}, [selectedTemplate, materialMode, selectedMaterials, smartSelectedIds, templateSegments])
// 组件卸载时取消未完成请求
useEffect(() => {
return () => {
if (clipsTimerRef.current) clearTimeout(clipsTimerRef.current)
if (clipsAbortRef.current) clipsAbortRef.current.abort()
}
}, [])
/* ── 手动选择素材 ── */ /* ── 手动选择素材 ── */
const handleToggleMaterial = useCallback( const handleToggleMaterial = useCallback(
(materialId: string) => { (materialId: string) => {
@@ -4,17 +4,24 @@ import { getTitles } from "@/api/titles"
import type { TitleSettings } from "../../types" import type { TitleSettings } from "../../types"
import { useAiTitleGenerator } from "./useAiTitleGenerator" import { useAiTitleGenerator } from "./useAiTitleGenerator"
import { useTitleStyleUpdaters } from "./useTitleStyleUpdaters" import { useTitleStyleUpdaters } from "./useTitleStyleUpdaters"
import { useDraftAutoSave } from "../useDraftAutoSave"
interface UseStep4TitleProps { interface UseStep4TitleProps {
titleSettings: TitleSettings titleSettings: TitleSettings
onTitleSettingsChange: (settings: TitleSettings) => void onTitleSettingsChange: (settings: TitleSettings) => void
/** 当前选中的模板/草稿 ID,用于自动保存 */
selectedTemplate?: string
} }
/** /**
* Step 4 标题设置 Hook * Step 4 标题设置 Hook
* 封装 AI 标题生成、标题样式设置等逻辑 * 封装 AI 标题生成、标题样式设置等逻辑
*/ */
export function useStep4Title({ titleSettings, onTitleSettingsChange }: UseStep4TitleProps) { export function useStep4Title({
titleSettings,
onTitleSettingsChange,
selectedTemplate,
}: UseStep4TitleProps) {
// 标题库数据 // 标题库数据
const { data: userTitles = [] } = useQuery({ const { data: userTitles = [] } = useQuery({
queryKey: ["titles"], queryKey: ["titles"],
@@ -42,6 +49,38 @@ export function useStep4Title({ titleSettings, onTitleSettingsChange }: UseStep4
const prevAiAutoSelect = useRef(titleSettings.aiAutoSelect) const prevAiAutoSelect = useRef(titleSettings.aiAutoSelect)
const isFirstMount = useRef(true) const isFirstMount = useRef(true)
/* ── Step4 标题内容/样式变化后自动保存草稿(防抖 800ms,失败静默) ── */
const { scheduleSave: scheduleTitleSave } = useDraftAutoSave(selectedTemplate)
useEffect(() => {
if (!selectedTemplate) return
scheduleTitleSave(
{
title: {
text: titleSettings.title,
font: titleSettings.font,
font_size: titleSettings.size,
color: titleSettings.color,
position: titleSettings.position,
bold: titleSettings.bold,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
},
},
800,
)
}, [
selectedTemplate,
titleSettings.title,
titleSettings.font,
titleSettings.size,
titleSettings.color,
titleSettings.position,
titleSettings.bold,
titleSettings.stroke,
titleSettings.shadow,
scheduleTitleSave,
])
// 当 AI 自动选择开关打开时,自动生成/选择一个标题填入 // 当 AI 自动选择开关打开时,自动生成/选择一个标题填入
// 首次挂载时如果开关已经是 true 且无标题,也需要触发 // 首次挂载时如果开关已经是 true 且无标题,也需要触发
useEffect(() => { useEffect(() => {
@@ -2,11 +2,13 @@
* Step 6 封面设置 Hook * Step 6 封面设置 Hook
* 封装封面设置的交互逻辑,对接后端封面模板 CRUD API * 封装封面设置的交互逻辑,对接后端封面模板 CRUD API
*/ */
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useRef, useState } from "react"
import { message } from "antd" import { message } from "antd"
import type { CoverConfig, CoverTemplate } from "../types/cover" import type { CoverConfig, CoverTemplate } from "../types/cover"
import { generateCover } from "@/api/generation" import { generateCover } from "@/api/generation"
import { createPreview, getPreviewStatus } from "@/api/generation/preview" import { createPreview, getPreviewStatus } from "@/api/generation/preview"
import { updateEditPlan } from "@/api/template-editor"
import type { TitleSettings } from "../types"
import { import {
fetchCoverTemplates, fetchCoverTemplates,
createCoverTemplate, createCoverTemplate,
@@ -22,6 +24,22 @@ interface UseStep6CoverProps {
assetIds?: string[] assetIds?: string[]
/** 当前选中的模板 ID */ /** 当前选中的模板 ID */
selectedTemplate?: string selectedTemplate?: string
/** Step4 标题设置,用于预览视频烧录标题 & 封面叠加标题 */
titleSettings?: TitleSettings
/** 预览任务创建回调——将 task_id 暴露给父组件供 confirmGeneration 复用 */
onPreviewTaskCreated?: (taskId: string) => void
/** 从预览响应中提取到 source_edit_plan_id 时的回调 */
onSourceEditPlanIdExtracted?: (planId: string) => void
/** 配音模式 */
voiceMode?: "preset" | "custom" | "clone"
/** 选中的配音素材 ID(配音素材库 asset ID */
selectedVoice?: string
/** 选中的克隆音色 ID */
selectedClonedVoice?: string
/** BGM 开关 */
bgm?: boolean
/** BGM 配置(来自模板) */
bgmConfig?: { enabled: boolean; music_id?: string }
} }
export function useStep6Cover({ export function useStep6Cover({
@@ -30,8 +48,18 @@ export function useStep6Cover({
duration, duration,
assetIds = [], assetIds = [],
selectedTemplate = "", selectedTemplate = "",
titleSettings,
onPreviewTaskCreated,
onSourceEditPlanIdExtracted,
voiceMode,
selectedVoice,
selectedClonedVoice,
bgm,
bgmConfig,
}: UseStep6CoverProps) { }: UseStep6CoverProps) {
const [generating, setGenerating] = useState(false) const [generating, setGenerating] = useState(false)
// 防竞态:记录当前预览生成的参数指纹,任务完成时校验一致性
const previewParamsRef = useRef<string>("")
// ── 封面设置弹窗状态 ── // ── 封面设置弹窗状态 ──
const [showCoverSettings, setShowCoverSettings] = useState(false) const [showCoverSettings, setShowCoverSettings] = useState(false)
@@ -92,6 +120,20 @@ export function useStep6Cover({
const response = await generateCover(selectedTemplate, { const response = await generateCover(selectedTemplate, {
asset_ids: assetIds, asset_ids: assetIds,
cover_type: "ai_frame", cover_type: "ai_frame",
...(titleSettings?.title
? {
title_config: {
text: titleSettings.title,
font: titleSettings.font,
font_size: titleSettings.size,
font_color: titleSettings.color,
position: titleSettings.position,
bold: titleSettings.bold,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
},
}
: {}),
}) })
clearTimeout(timeoutId) clearTimeout(timeoutId)
const thumbnailUrl = response.cover?.image_url || "" const thumbnailUrl = response.cover?.image_url || ""
@@ -132,11 +174,46 @@ export function useStep6Cover({
console.log("[Step6] 检测到预览缺失,尝试自动创建预览渲染任务...") console.log("[Step6] 检测到预览缺失,尝试自动创建预览渲染任务...")
message.info("正在准备预览视频,请稍候...") message.info("正在准备预览视频,请稍候...")
try { try {
// 记录当前参数指纹,用于任务完成时校验一致性(防竞态)
previewParamsRef.current = JSON.stringify({ selectedTemplate, assetIds, titleSettings })
// 解析配音参数:voiceMode=clone 时用 selectedClonedVoice,否则用 selectedVoice
const previewVoiceLibraryId =
voiceMode === "clone" ? selectedClonedVoice || "" : selectedVoice || ""
const previewResp = await createPreview({ const previewResp = await createPreview({
template_id: selectedTemplate, template_id: selectedTemplate,
asset_ids: assetIds, asset_ids: assetIds,
duration: duration || 30, duration: duration || 30,
// 配音:voice_library_id 是配音素材库 asset ID(用户上传的音频或 AI 配音)
...(previewVoiceLibraryId ? { voice_library_id: previewVoiceLibraryId } : {}),
// BGM 配置:受 bgm 开关控制
bgm_config: {
enabled: bgm !== false,
...(bgmConfig?.music_id ? { preset_id: bgmConfig.music_id } : {}),
},
...(titleSettings?.title
? {
title_config: {
text: titleSettings.title,
font: titleSettings.font,
font_size: titleSettings.size,
font_color: titleSettings.color,
position: titleSettings.position,
bold: titleSettings.bold,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
},
}
: {}),
}) })
// 将预览任务 ID 暴露给父组件,供 Step7 确认生成时复用(confirmGeneration
const currentFingerprint = JSON.stringify({ selectedTemplate, assetIds, titleSettings })
if (previewResp.task_id && previewParamsRef.current === currentFingerprint) {
onPreviewTaskCreated?.(previewResp.task_id)
// 提取后端自动关联的 source_edit_plan_id,供 fallback 路径使用
if (previewResp.source_edit_plan_id) {
onSourceEditPlanIdExtracted?.(previewResp.source_edit_plan_id)
}
}
// 轮询等待预览渲染完成:递归 setTimeout 避免请求重叠 + 120s 超时兜底 // 轮询等待预览渲染完成:递归 setTimeout 避免请求重叠 + 120s 超时兜底
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
let finished = false let finished = false
@@ -154,6 +231,21 @@ export function useStep6Cover({
try { try {
const status = await getPreviewStatus(previewResp.task_id) const status = await getPreviewStatus(previewResp.task_id)
if (status.status === "completed") { if (status.status === "completed") {
// 保存预览视频地址到 plan.config.rendered_storage_key
// 供封面 API 的 E1 兜底路径定位渲染后的视频(含标题烧录)。
// video_url 可能是完整 http(s) URL 或 OSS storage_key,两种格式后端都能处理。
if (status.video_url) {
try {
await updateEditPlan(selectedTemplate, {
config: { rendered_storage_key: status.video_url },
})
} catch (saveErr) {
console.warn(
"[Step6] 保存 rendered_storage_key 失败(不阻塞封面重试):",
saveErr,
)
}
}
done(() => resolve()) done(() => resolve())
} else if (status.status === "failed") { } else if (status.status === "failed") {
done(() => reject(new Error(status.error_message || "预览渲染失败"))) done(() => reject(new Error(status.error_message || "预览渲染失败")))
@@ -171,6 +263,20 @@ export function useStep6Cover({
const retryResp = await generateCover(selectedTemplate, { const retryResp = await generateCover(selectedTemplate, {
asset_ids: assetIds, asset_ids: assetIds,
cover_type: "ai_frame", cover_type: "ai_frame",
...(titleSettings?.title
? {
title_config: {
text: titleSettings.title,
font: titleSettings.font,
font_size: titleSettings.size,
font_color: titleSettings.color,
position: titleSettings.position,
bold: titleSettings.bold,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
},
}
: {}),
}) })
const retryUrl = retryResp.cover?.image_url || "" const retryUrl = retryResp.cover?.image_url || ""
if (retryUrl) { if (retryUrl) {
@@ -212,7 +318,22 @@ export function useStep6Cover({
clearTimeout(timeoutId) clearTimeout(timeoutId)
setGenerating(false) setGenerating(false)
} }
}, [selectedTemplate, assetIds, coverSettings, onCoverSettingsChange, generating, duration]) }, [
selectedTemplate,
assetIds,
coverSettings,
onCoverSettingsChange,
generating,
duration,
titleSettings,
onPreviewTaskCreated,
onSourceEditPlanIdExtracted,
voiceMode,
selectedVoice,
selectedClonedVoice,
bgm,
bgmConfig,
])
// ── 模板操作方法 ── // ── 模板操作方法 ──
const handleSelectTemplate = useCallback((id: string) => { const handleSelectTemplate = useCallback((id: string) => {
@@ -3,7 +3,7 @@
* 管理步骤切换与各步骤的前置校验 * 管理步骤切换与各步骤的前置校验
* 步骤顺序:模板(1) → 素材(2) → 配音(3) → 标题(4) → 预览(5) → 封面(6) → 确认(7) * 步骤顺序:模板(1) → 素材(2) → 配音(3) → 标题(4) → 预览(5) → 封面(6) → 确认(7)
* *
* V24: previewReady 改为前端素材加载状态 * 前端实时预览架构:Step5 无需等待服务器渲染
*/ */
import { message } from "antd" import { message } from "antd"
import type { TitleSettings } from "../types" import type { TitleSettings } from "../types"
@@ -16,8 +16,6 @@ export interface UseStepNavigationOptions {
selectedMaterials: string[] selectedMaterials: string[]
smartSelectedIds: string[] smartSelectedIds: string[]
titleSettings: TitleSettings titleSettings: TitleSettings
/** 预览是否就绪(前端素材已加载) */
previewReady: boolean
} }
export interface UseStepNavigationReturn { export interface UseStepNavigationReturn {
@@ -34,7 +32,6 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav
selectedMaterials, selectedMaterials,
smartSelectedIds, smartSelectedIds,
titleSettings, titleSettings,
previewReady,
} = options } = options
const goNext = () => { const goNext = () => {
@@ -50,15 +47,10 @@ export const useStepNavigation = (options: UseStepNavigationOptions): UseStepNav
message.warning("请先进行智能匹配并选择素材") message.warning("请先进行智能匹配并选择素材")
return return
} }
// Step3 配音:配音为可选项,不强制校验,用户可跳过
if (currentStep === 4 && !titleSettings.title.trim()) { if (currentStep === 4 && !titleSettings.title.trim()) {
message.warning("请选择或输入标题") message.warning("请选择或输入标题")
return return
} }
if (currentStep === 5 && !previewReady) {
message.warning("请先选择素材以预览效果")
return
}
if (currentStep < 7) { if (currentStep < 7) {
setCurrentStep((s) => s + 1) setCurrentStep((s) => s + 1)
} }
@@ -0,0 +1,69 @@
/**
* 将选中素材 + 模板 segments 构建为 edit_plan_clips 写入数据。
*
* 逻辑必须与 FrontendPreviewPlayer.tsx 中 buildPlaybackSegments 完全一致:
* assetDuration = asset.duration || asset.metadata?.duration || 30
* tplSeg = templateSegments[i] || lastSegment
* segDuration = clamp(assetDuration, tplSeg.duration_min, tplSeg.duration_max)
* start_time = 0
* // 关键:预览播放器中 endTime = min(startTime + segDuration, assetDuration)
* // 因此 clips.duration 也必须用 min(segDuration, assetDuration) 截断,
* // 避免素材实际时长比 clamp 后的 segDuration 短时,Worker 尝试读取不存在的片段
* duration = min(segDuration, assetDuration)
*
* 预览播放器(Canvas 实时预览)直接在内存中构建 segments 播放,不读 edit_plan_clips
* 本函数产出的 clips 写入 DB 后由 Worker 渲染。两边用完全相同的时长计算,
* 保证用户在编辑过程中看到的预览与最终生成视频一致。
*/
import type { AssetItem } from "@/api/assets"
import type { TemplateSegment } from "@/api/templates/types"
import type { EditPlanClipInput } from "@/api/template-editor"
interface BuildClipsOptions {
/** 选中的素材 ID 列表(按选择顺序) */
selectedIds: string[]
/** 已加载的素材列表(用于查 duration) */
materials: AssetItem[]
/** 智能匹配返回的素材(auto 模式下可能不在 materials 列表中) */
smartMatchedAssets?: AssetItem[]
/** 模板 segments */
templateSegments?: TemplateSegment[]
}
export function buildClipsFromAssets({
selectedIds,
materials,
smartMatchedAssets = [],
templateSegments = [],
}: BuildClipsOptions): EditPlanClipInput[] {
if (!selectedIds.length) return []
// 合并两个素材来源,建立 id → asset 索引
const assetMap = new Map<string, AssetItem>()
for (const a of materials) assetMap.set(a.id, a)
for (const a of smartMatchedAssets) assetMap.set(a.id, a)
const lastSeg = templateSegments[templateSegments.length - 1]
return selectedIds.map((assetId, i) => {
const asset = assetMap.get(assetId)
const assetDuration = asset?.duration || asset?.metadata?.duration || 30
const tplSeg = templateSegments[i] || lastSeg
const segDuration = tplSeg
? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration))
: Math.min(assetDuration, 10)
// 与 FrontendPreviewPlayer.buildPlaybackSegments 中
// endTime = Math.min(startTime + segDuration, assetDuration)
// 保持一致:duration 不能超过素材实际时长
const duration = Math.min(segDuration, assetDuration)
return {
asset_id: assetId,
start_time: 0,
duration,
order: i,
}
})
}
@@ -0,0 +1,46 @@
/**
* 根据视频比例计算输出分辨率
*
* 规则:
* - 长边固定 1920
* - 短边按 1920 × (短/长) 计算,对齐到偶数
* - 9:16 → 1080×1920(竖屏)
* - 16:9 → 1920×1080(横屏)
* - 1:1 → 1920×1920
* - 无法解析时 fallback 到 1080×1920
*/
export interface Resolution {
width: number
height: number
}
export function calculateResolution(ratio: string): Resolution {
if (!ratio) return { width: 1080, height: 1920 }
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) {
// 竖屏:短边是宽,长边是高
return { width: evenShort, height: baseLong }
} else {
// 横屏:长边是宽,短边是高
return { width: baseLong, height: evenShort }
}
}
return { width: 1080, height: 1920 }
}
if (ratio.includes("x")) {
const [wStr, hStr] = ratio.split("x")
const w = parseInt(wStr, 10) || 1080
const h = parseInt(hStr, 10) || 1920
return { width: w, height: h }
}
return { width: 1080, height: 1920 }
}
@@ -87,7 +87,7 @@ export const useTemplateLibrary = () => {
[copyMutation], [copyMutation],
) )
/* 操作:使用模板 → 跳转剪辑编辑器 */ /* 操作:使用模板 → 跳转剪辑模板 */
const handleUse = useCallback( const handleUse = useCallback(
(template: TemplateItem) => { (template: TemplateItem) => {
navigate(`/app/editing-planner?templateId=${template.id}`) navigate(`/app/editing-planner?templateId=${template.id}`)
+2 -2
View File
@@ -32,7 +32,7 @@ describe("bgm API", () => {
describe("getBgmPresets", () => { describe("getBgmPresets", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(getBgmPresets("test-params?")).resolves.not.toThrow() await expect(getBgmPresets("test-template", { category: "test" })).resolves.not.toThrow()
}) })
it("should reject on API error", async () => { it("should reject on API error", async () => {
@@ -42,7 +42,7 @@ describe("bgm API", () => {
mockDelete.mockRejectedValue(new Error("Network error")) mockDelete.mockRejectedValue(new Error("Network error"))
mockPatch.mockRejectedValue(new Error("Network error")) mockPatch.mockRejectedValue(new Error("Network error"))
await expect(getBgmPresets("test-params?")).rejects.toThrow() await expect(getBgmPresets("test-template", { category: "test" })).rejects.toThrow()
}) })
}) })
}) })
-136
View File
@@ -1,16 +1,9 @@
import { describe, expect, it, vi, beforeEach } from "vitest" import { describe, expect, it, vi, beforeEach } from "vitest"
import { import {
getEditPlans,
getEditPlan, getEditPlan,
createEditPlan,
updateEditPlan, updateEditPlan,
deleteEditPlan,
generateEditPlan,
getGenerationStatus,
aiRecommendClips, aiRecommendClips,
getEditPlanGenerations,
getGenerationTaskResults, getGenerationTaskResults,
cancelGeneration,
getEditPlanClips, getEditPlanClips,
getEditPlanClip, getEditPlanClip,
createEditPlanClip, createEditPlanClip,
@@ -19,7 +12,6 @@ import {
reorderEditPlanClips, reorderEditPlanClips,
batchDeleteEditPlanClips, batchDeleteEditPlanClips,
createClipsFromAssets, createClipsFromAssets,
copyEditPlan,
getMediaAssets, getMediaAssets,
getMediaAsset, getMediaAsset,
} from "@/api/template-editor" } from "@/api/template-editor"
@@ -53,22 +45,6 @@ describe("editPlans API", () => {
mockPatch.mockResolvedValue({ data: { success: true, items: [] } }) mockPatch.mockResolvedValue({ data: { success: true, items: [] } })
}) })
describe("getEditPlans", () => {
it("should resolve successfully", async () => {
await expect(getEditPlans("test-params?")).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(getEditPlans("test-params?")).rejects.toThrow()
})
})
describe("getEditPlan", () => { describe("getEditPlan", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(getEditPlan("test-planId")).resolves.not.toThrow() await expect(getEditPlan("test-planId")).resolves.not.toThrow()
@@ -85,22 +61,6 @@ describe("editPlans API", () => {
}) })
}) })
describe("createEditPlan", () => {
it("should resolve successfully", async () => {
await expect(createEditPlan({ 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(createEditPlan({ name: "test-item" })).rejects.toThrow()
})
})
describe("updateEditPlan", () => { describe("updateEditPlan", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(updateEditPlan("test-planId")).resolves.not.toThrow() await expect(updateEditPlan("test-planId")).resolves.not.toThrow()
@@ -117,54 +77,6 @@ describe("editPlans API", () => {
}) })
}) })
describe("deleteEditPlan", () => {
it("should resolve successfully", async () => {
await expect(deleteEditPlan("test-planId")).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(deleteEditPlan("test-planId")).rejects.toThrow()
})
})
describe("generateEditPlan", () => {
it("should resolve successfully", async () => {
await expect(generateEditPlan("test-planId")).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(generateEditPlan("test-planId")).rejects.toThrow()
})
})
describe("getGenerationStatus", () => {
it("should resolve successfully", async () => {
await expect(getGenerationStatus("test-planId")).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(getGenerationStatus("test-planId")).rejects.toThrow()
})
})
describe("aiRecommendClips", () => { describe("aiRecommendClips", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(aiRecommendClips("test-planId")).resolves.not.toThrow() await expect(aiRecommendClips("test-planId")).resolves.not.toThrow()
@@ -181,22 +93,6 @@ describe("editPlans API", () => {
}) })
}) })
describe("getEditPlanGenerations", () => {
it("should resolve successfully", async () => {
await expect(getEditPlanGenerations("test-planId")).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(getEditPlanGenerations("test-planId")).rejects.toThrow()
})
})
describe("getGenerationTaskResults", () => { describe("getGenerationTaskResults", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(getGenerationTaskResults("test-taskId")).resolves.not.toThrow() await expect(getGenerationTaskResults("test-taskId")).resolves.not.toThrow()
@@ -213,22 +109,6 @@ describe("editPlans API", () => {
}) })
}) })
describe("cancelGeneration", () => {
it("should resolve successfully", async () => {
await expect(cancelGeneration("test-planId")).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(cancelGeneration("test-planId")).rejects.toThrow()
})
})
describe("getEditPlanClips", () => { describe("getEditPlanClips", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(getEditPlanClips("test-planId")).resolves.not.toThrow() await expect(getEditPlanClips("test-planId")).resolves.not.toThrow()
@@ -357,22 +237,6 @@ describe("editPlans API", () => {
}) })
}) })
describe("copyEditPlan", () => {
it("should resolve successfully", async () => {
await expect(copyEditPlan("test-planId")).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(copyEditPlan("test-planId")).rejects.toThrow()
})
})
describe("getMediaAssets", () => { describe("getMediaAssets", () => {
it("should resolve successfully", async () => { it("should resolve successfully", async () => {
await expect(getMediaAssets("test-libraryId?")).resolves.not.toThrow() await expect(getMediaAssets("test-libraryId?")).resolves.not.toThrow()
-17
View File
@@ -5,7 +5,6 @@ import {
getTemplate, getTemplate,
toggleFavoriteTemplate, toggleFavoriteTemplate,
copyTemplate, copyTemplate,
generateFromTemplate,
} from "@/api/templates" } from "@/api/templates"
const mockGet = vi.fn() const mockGet = vi.fn()
@@ -116,20 +115,4 @@ describe("templates API", () => {
await expect(copyTemplate("test-templateId")).rejects.toThrow() await expect(copyTemplate("test-templateId")).rejects.toThrow()
}) })
}) })
describe("generateFromTemplate", () => {
it("should resolve successfully", async () => {
await expect(generateFromTemplate("test-templateId")).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(generateFromTemplate("test-templateId")).rejects.toThrow()
})
})
}) })
@@ -148,14 +148,9 @@ vi.mock("@/api/editing-planner", () => ({
vi.mock("@/api/template-editor", () => ({ vi.mock("@/api/template-editor", () => ({
getMediaAssets: vi.fn().mockResolvedValue({ items: [] }), getMediaAssets: vi.fn().mockResolvedValue({ items: [] }),
getEditPlanGenerations: vi.fn().mockResolvedValue({ items: [] }),
getEditPlan: vi.fn().mockResolvedValue({}), getEditPlan: vi.fn().mockResolvedValue({}),
createEditPlan: vi.fn().mockResolvedValue({ id: "test-plan" }),
updateEditPlan: vi.fn().mockResolvedValue({}), updateEditPlan: vi.fn().mockResolvedValue({}),
generateEditPlan: vi.fn().mockResolvedValue({ task_id: "test-task" }),
getGenerationStatus: vi.fn().mockResolvedValue({ status: "completed" }),
getGenerationTaskResults: vi.fn().mockResolvedValue({ items: [] }), getGenerationTaskResults: vi.fn().mockResolvedValue({ items: [] }),
cancelGeneration: vi.fn().mockResolvedValue({}),
getEditPlanClips: vi.fn().mockResolvedValue({ items: [] }), getEditPlanClips: vi.fn().mockResolvedValue({ items: [] }),
createEditPlanClip: vi.fn().mockResolvedValue({}), createEditPlanClip: vi.fn().mockResolvedValue({}),
batchDeleteEditPlanClips: vi.fn().mockResolvedValue({}), batchDeleteEditPlanClips: vi.fn().mockResolvedValue({}),
@@ -225,12 +220,6 @@ vi.mock("@/pages/editing-planner/components/StickerPanel", () => ({
vi.mock("@/pages/editing-planner/components/SaveModal", () => ({ vi.mock("@/pages/editing-planner/components/SaveModal", () => ({
default: () => React.createElement("div", { "data-testid": "SaveModal" }), default: () => React.createElement("div", { "data-testid": "SaveModal" }),
})) }))
vi.mock("@/pages/editing-planner/components/GenerationHistoryModal", () => ({
default: () => React.createElement("div", { "data-testid": "GenerationHistoryModal" }),
}))
vi.mock("@/pages/editing-planner/components/GenerationProgressModal", () => ({
default: () => React.createElement("div", { "data-testid": "GenerationProgressModal" }),
}))
// === useUndoRedo hook mock === // === useUndoRedo hook mock ===
vi.mock("@/pages/editing-planner/hooks/useUndoRedo", () => ({ vi.mock("@/pages/editing-planner/hooks/useUndoRedo", () => ({
@@ -214,12 +214,6 @@ vi.mock("@/api/titles", () => ({
})) }))
vi.mock("@/api/template-editor", () => ({ vi.mock("@/api/template-editor", () => ({
generateEditPlan: vi.fn().mockResolvedValue({
plan_id: "test-plan",
generation_task_id: "test-task",
plan_status: "processing",
clip_count: 5,
}),
updateEditPlan: vi.fn().mockResolvedValue({ plan_id: "test-plan", template_id: "test-template" }), updateEditPlan: vi.fn().mockResolvedValue({ plan_id: "test-plan", template_id: "test-template" }),
getEditPlan: vi.fn().mockResolvedValue({ getEditPlan: vi.fn().mockResolvedValue({
plan_id: "test-plan", plan_id: "test-plan",
@@ -228,9 +222,6 @@ vi.mock("@/api/template-editor", () => ({
config: {}, config: {},
status: "draft", status: "draft",
}), }),
getGenerationStatus: vi
.fn()
.mockResolvedValue({ plan_status: "completed", generation_task_id: "test-task", clips: [] }),
getGenerationTaskResults: vi.fn().mockResolvedValue({ items: [] }), getGenerationTaskResults: vi.fn().mockResolvedValue({ items: [] }),
})) }))
@@ -109,7 +109,6 @@ vi.mock("@/api/templates", () => ({
getTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), getTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
toggleFavoriteTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), toggleFavoriteTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
copyTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }), copyTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
generateFromTemplate: vi.fn().mockResolvedValue({ items: [], total: 0, success: true }),
})) }))
vi.mock("@/pages/templates/TemplateLibrary.css", () => ({})) vi.mock("@/pages/templates/TemplateLibrary.css", () => ({}))
@@ -1,39 +0,0 @@
import { describe, expect, it, vi } from "vitest"
import { render } from "@testing-library/react"
import GenerationHistoryModal from "@/pages/editing-planner/components/GenerationHistoryModal"
const defaultProps = {
open: true,
loading: false,
history: [],
onClose: vi.fn(),
onCancel: vi.fn(),
cancelLoading: false,
}
describe("GenerationHistoryModal", () => {
it("should render without crashing", () => {
const { container } = render(<GenerationHistoryModal {...defaultProps} />)
expect(container).toBeTruthy()
})
it("should not render when open is false", () => {
const { container } = render(<GenerationHistoryModal {...defaultProps} open={false} />)
expect(container.firstChild).toBeNull()
})
it("should render with history items", () => {
const history = [
{
id: "1",
status: "completed",
created_at: "2024-01-01T00:00:00Z",
duration: 60,
},
]
const { container } = render(
<GenerationHistoryModal {...defaultProps} history={history as any} />,
)
expect(container).toBeTruthy()
})
})
@@ -1,102 +0,0 @@
import { describe, expect, it, vi } from "vitest"
import { render } from "@testing-library/react"
import GenerationProgressModal from "@/pages/editing-planner/components/GenerationProgressModal"
const baseProps = {
open: true,
voiceoverDuration: null,
estimatedDuration: 60,
onDurationChange: vi.fn(),
onGenerate: vi.fn(),
task: null,
submitting: false,
onCancel: vi.fn(),
onRetry: vi.fn(),
onClose: vi.fn(),
}
describe("GenerationProgressModal", () => {
it("should render setup phase", () => {
const { container } = render(<GenerationProgressModal {...baseProps} phase="setup" />)
expect(container).toBeTruthy()
})
it("should render progress phase without task", () => {
const { container } = render(<GenerationProgressModal {...baseProps} phase="progress" />)
expect(container).toBeTruthy()
})
it("should render progress phase with task data", () => {
const { container } = render(
<GenerationProgressModal
{...baseProps}
phase="progress"
task={
{
id: "task-123",
status: "generating_video",
progress: 50,
current_step: "generating_video",
user_message: "正在生成视频",
} as any
}
/>,
)
expect(container).toBeTruthy()
})
it("should render completed phase", () => {
const { container } = render(
<GenerationProgressModal
{...baseProps}
phase="completed"
task={{ id: "task-1", status: "completed", progress: 100 } as any}
/>,
)
expect(container).toBeTruthy()
})
it("should render failed phase with retry", () => {
const { container } = render(
<GenerationProgressModal
{...baseProps}
phase="failed"
task={
{
id: "task-1",
status: "failed",
progress: 30,
error_message: "生成失败",
retryable: true,
} as any
}
/>,
)
expect(container).toBeTruthy()
})
it("should render failed phase without retry", () => {
const { container } = render(
<GenerationProgressModal
{...baseProps}
phase="failed"
task={
{
id: "task-1",
status: "failed",
progress: 30,
retryable: false,
} as any
}
/>,
)
expect(container).toBeTruthy()
})
it("should not render when closed", () => {
const { container } = render(
<GenerationProgressModal {...baseProps} phase="setup" open={false} />,
)
expect(container).toBeTruthy()
})
})
@@ -31,8 +31,6 @@ import "@/pages/editing-planner/components/ClipPropertiesPanel"
import "@/pages/editing-planner/components/EditorClipList" import "@/pages/editing-planner/components/EditorClipList"
import "@/pages/editing-planner/components/EditingDrawers" import "@/pages/editing-planner/components/EditingDrawers"
import "@/pages/editing-planner/components/FilterPanel" import "@/pages/editing-planner/components/FilterPanel"
import "@/pages/editing-planner/components/GenerationHistoryModal"
import "@/pages/editing-planner/components/GenerationProgressModal"
import "@/pages/editing-planner/components/GreenScreenPanel" import "@/pages/editing-planner/components/GreenScreenPanel"
import "@/pages/editing-planner/components/IntroOutroPanel" import "@/pages/editing-planner/components/IntroOutroPanel"
import "@/pages/editing-planner/components/MediaPanel" import "@/pages/editing-planner/components/MediaPanel"
+1 -1
View File
@@ -7,7 +7,7 @@ VideoProcessor 等)按需从子模块导入,避免 __init__ 阶段引入
packages / DB 等重依赖。 packages / DB 等重依赖。
""" """
# 共享工具模块(零外部依赖,供 editing_modes / generation / edit_plan_generation 等复用) # 共享工具模块(零外部依赖,供 editing_modes / generation 等复用)
from . import dedup_helpers, ffmpeg_utils, oss_helpers, url_security from . import dedup_helpers, ffmpeg_utils, oss_helpers, url_security
__all__ = [ __all__ = [
@@ -1,6 +1,6 @@
"""查重辅助函数 — 从 generation.py 提取的 GeneratedVideo 记录 + 查重逻辑. """查重辅助函数 — 从 generation.py 提取的 GeneratedVideo 记录 + 查重逻辑.
render_edit_plan 和 generate_video 共同复用, 供 generate_video 共同复用,
创建 GeneratedVideo 记录后计算指纹并执行项目级 + 批次内查重。 创建 GeneratedVideo 记录后计算指纹并执行项目级 + 批次内查重。
""" """
+1 -1
View File
@@ -1,4 +1,4 @@
"""OSS 工具函数 — 从 generation.py / edit_plan_generation.py 提取的共享 OSS 操作. """OSS 工具函数 — 从 generation.py 提取的共享 OSS 操作.
提供 OSS 配置读取、Bucket 创建、素材上传/下载、asset_id → 本地路径解析 提供 OSS 配置读取、Bucket 创建、素材上传/下载、asset_id → 本地路径解析
等能力,供 render_edit_plan 和 generate_video 共同复用。 等能力,供 render_edit_plan 和 generate_video 共同复用。
+14 -8
View File
@@ -84,6 +84,7 @@ class RenderAdapterResult:
cover_candidates: list[dict] | None = ( cover_candidates: list[dict] | None = (
None # 封面候选帧 [{"image_url": "...", "frame_time": 5.0, "storage_key": "..."}] None # 封面候选帧 [{"image_url": "...", "frame_time": 5.0, "storage_key": "..."}]
) )
temp_dir: str | None = None # 渲染临时目录,成功时由调用方清理,失败时由 finally 清理
def __post_init__(self): def __post_init__(self):
if self.rendered_clip_ids is None: if self.rendered_clip_ids is None:
@@ -128,6 +129,7 @@ class RenderAdapter:
job_id: str = "", job_id: str = "",
work_dir: Path | None = None, work_dir: Path | None = None,
progress_cb: ProgressCallback | None = None, progress_cb: ProgressCallback | None = None,
voiceover_audio_path: str | None = None,
) -> RenderAdapterResult: ) -> RenderAdapterResult:
"""渲染一个 EditPlan。 """渲染一个 EditPlan。
@@ -142,6 +144,7 @@ class RenderAdapter:
job_id: 关联的 Job ID(用于结果存储路径) job_id: 关联的 Job ID(用于结果存储路径)
work_dir: 工作目录,不传则使用临时目录 work_dir: 工作目录,不传则使用临时目录
progress_cb: 进度回调函数 progress_cb: 进度回调函数
voiceover_audio_path: 配音音频本地路径(一键生成场景使用)
Returns: Returns:
RenderAdapterResult RenderAdapterResult
@@ -198,7 +201,7 @@ class RenderAdapter:
self._report_progress(progress_cb, 35.0, "准备 BGM 音频") self._report_progress(progress_cb, 35.0, "准备 BGM 音频")
# 3~6. 统一渲染核心流程(BGM + ASR + 渲染 + 缩略图 + 上传) # 3~6. 统一渲染核心流程(BGM + ASR + 渲染 + 缩略图 + 上传)
return self._do_render( result = self._do_render(
plan=plan, plan=plan,
clips=ready_clips, clips=ready_clips,
asset_path_map=asset_path_map, asset_path_map=asset_path_map,
@@ -208,7 +211,13 @@ class RenderAdapter:
progress_cb=progress_cb, progress_cb=progress_cb,
rendered_clip_ids=rendered_clip_ids, rendered_clip_ids=rendered_clip_ids,
failed_clip_ids=failed_clip_ids, failed_clip_ids=failed_clip_ids,
voiceover_audio_path=voiceover_audio_path,
) )
# 成功时将临时目录所有权转移给调用方,阻止 finally 清理
if result.success and temp_dir:
result.temp_dir = temp_dir
temp_dir = None # 阻止 finally 块清理
return result
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
stderr_text = (exc.stderr or "").strip() stderr_text = (exc.stderr or "").strip()
@@ -585,14 +594,11 @@ class RenderAdapter:
try: try:
from video_processing.thumbnail_generator import extract_and_upload_cover_frames from video_processing.thumbnail_generator import extract_and_upload_cover_frames
# 从 plan config 提取标题文字,叠加到封面候选帧上 # 已渲染视频在统一渲染阶段已通过 ASS 字幕把标题烧录进画面,
_title_cfg = (plan_config or {}).get("title", {}) or {} # 抽帧天然带标题,因此这里传空字符串,避免 Pillow 二次叠加导致重影。
if not isinstance(_title_cfg, dict): # Pillow 叠加仅用于 API 从源素材抽帧(源素材本身无标题)的兜底场景。
_title_cfg = {}
_title_text = (_title_cfg.get("text", "") or "").strip() if _title_cfg.get("enabled", True) else ""
cover_candidates = extract_and_upload_cover_frames( cover_candidates = extract_and_upload_cover_frames(
str(result.output_path), plan_id, num_frames=3, title_text=_title_text str(result.output_path), plan_id, num_frames=3, title_text=""
) )
if cover_candidates: if cover_candidates:
logger.info( logger.info(
+94 -32
View File
@@ -67,6 +67,16 @@ def clip_has_audio(ctx: RenderContext, clip: ResolvedClip) -> bool:
return ctx._audio_cache[key] return ctx._audio_cache[key]
def _clip_volume(clip: ResolvedClip) -> float:
"""读取 clip 的音量配置(0.0~1.0>1 放大)。缺省 1.0 原声。"""
cfg = getattr(clip, "config", None) or {}
try:
vol = float(cfg.get("volume", 1.0))
except (TypeError, ValueError):
return 1.0
return max(0.0, vol)
# ── 音频混音 ────────────────────────────────────────────────────────────────── # ── 音频混音 ──────────────────────────────────────────────────────────────────
@@ -82,12 +92,13 @@ def mix_audio(
"""音频后处理混音. """音频后处理混音.
处理逻辑: 处理逻辑:
1. 丢弃主图层(main/broll/overlay/corner_voice的原始音频,避免录入源视频杂音 1. 保留主图层(main/broll/overlay/corner_voice视频素材的原声,按顺序 concat 拼接
2. 仅使用独立音频轨(audio role,TTS/配音)作为主音频 2. 每个 clip 按 config.volume 应用音量(volume=0 静音,=1 原声)
3. 如果提供了 bgm_path,则额外混入 BGM(支持淡入淡出、循环、人声闪避) 3. 独立音频轨(audio roleTTS/配音)通过 amix 混入
4. 如果配置了 audio_tracks,则混入多轨道音频(配音、音效等 4. 如果提供了 bgm_path,则额外混入 BGM(支持淡入淡出、循环、人声闪避
5. 输出时长截断到 video_duration 5. 如果配置了 audio_tracks,则混入多轨道音频(配音、音效等)
6. 如果配置了降噪,最后应用降噪 6. 输出时长截断到 video_duration
7. 如果配置了降噪,最后应用降噪
Args: Args:
ctx: 渲染上下文 ctx: 渲染上下文
@@ -123,10 +134,12 @@ def mix_audio(
if "audio" in layer_map: if "audio" in layer_map:
audio_clips = layer_map["audio"].clips audio_clips = layer_map["audio"].clips
# ── 丢弃源视频的原始音频(避免录入杂音),成片仅保留 TTS 配音 + BGM ── # ── 保留源视频原声:过滤掉无音频流的 main clip(图片/无声素材) ──
main_clips = [] # 注意:volume=0 的 clip 不能移除——移除会导致后续 clip 音频时间轴前移、音画不同步。
# volume=0 通过滤镜链生成静音流,保持时间轴对齐。
main_clips = [c for c in main_clips if clip_has_audio(ctx, c)]
# ── 防御:过滤掉无音频流的 clip ── # ── 防御:过滤掉无音频流的独立音频轨 ──
audio_clips = [c for c in audio_clips if clip_has_audio(ctx, c)] audio_clips = [c for c in audio_clips if clip_has_audio(ctx, c)]
if not main_clips and not audio_clips: if not main_clips and not audio_clips:
@@ -144,7 +157,7 @@ def mix_audio(
# 构建音频处理命令 # 构建音频处理命令
output_path = ctx.work_dir / f"audio_{ctx.plan_id}.aac" output_path = ctx.work_dir / f"audio_{ctx.plan_id}.aac"
# 源视频原始音频已被丢弃(main_clips = []),最终音频完全由独立音频轨 + BGM + 多轨配置组成 # 主音频为视频素材原声 concat;独立音频轨(TTS/配音)通过 amix 混入
# 当无 main_clips 时,将独立音频轨作为主音频走 concat 拼接;当二者均有则走 amix 混音。 # 当无 main_clips 时,将独立音频轨作为主音频走 concat 拼接;当二者均有则走 amix 混音。
if main_clips: if main_clips:
effective_main = main_clips effective_main = main_clips
@@ -266,28 +279,67 @@ def concat_main_audio(
has_speed = abs(speed - 1.0) >= 1e-6 has_speed = abs(speed - 1.0) >= 1e-6
if not has_speed and not has_reverse: if not has_speed and not has_reverse:
# 无调速无倒放:简单命令行,-ss 裁剪更高效 # 无调速无倒放:根据是否需要裁剪/音量选择最高效的路径。
command = [ vol = _clip_volume(clip)
FFMPEG_BIN, need_trim = trim_start > 0 or (effective_duration > 0 and final_duration < adjusted_duration)
"-y", need_volume = abs(vol - 1.0) >= 1e-6
"-i",
str(clip.local_path), if need_trim:
"-vn", # 需要裁剪:用 atrim 滤镜在滤镜链中精确裁剪(采样点级精度,不浪费解码)。
"-acodec", # 滤镜顺序:atrim → asetpts → volume(先裁剪再调音量,避免处理被丢弃的数据)。
"aac", af_parts: list[str] = []
"-b:a", if trim_start > 0 and effective_duration > 0:
"128k", af_parts.append(f"atrim=start={trim_start:.3f}:duration={final_duration:.3f}")
"-ar", elif trim_start > 0:
"48000", af_parts.append(f"atrim=start={trim_start:.3f}")
"-ac", elif final_duration > 0:
"2", af_parts.append(f"atrim=duration={final_duration:.3f}")
] af_parts.append("asetpts=PTS-STARTPTS")
if trim_start > 0: if need_volume:
command.extend(["-ss", f"{trim_start:.3f}"]) af_parts.append(f"volume={vol:.4f}")
if final_duration > 0: command = [
command.extend(["-t", f"{final_duration:.3f}"]) FFMPEG_BIN,
command.append(str(output_path)) "-y",
run_ffmpeg(command) "-i",
str(clip.local_path),
"-vn",
"-af",
",".join(af_parts),
"-acodec",
"aac",
"-b:a",
"128k",
"-ar",
"48000",
"-ac",
"2",
]
# atrim 已精确控制时长,无需额外 -t
command.append(str(output_path))
run_ffmpeg(command)
else:
# 无需裁剪:直接提取,最高效。音量用单个 -af(如有)。
command = [
FFMPEG_BIN,
"-y",
"-i",
str(clip.local_path),
"-vn",
"-acodec",
"aac",
"-b:a",
"128k",
"-ar",
"48000",
"-ac",
"2",
]
if need_volume:
command.extend(["-af", f"volume={vol:.4f}"])
if final_duration > 0:
command.extend(["-t", f"{final_duration:.3f}"])
command.append(str(output_path))
run_ffmpeg(command)
else: else:
# 有调速或倒放:用 filter_complex # 有调速或倒放:用 filter_complex
speed_engine = SpeedEngine() speed_engine = SpeedEngine()
@@ -312,6 +364,11 @@ def concat_main_audio(
if reverse_filter: if reverse_filter:
audio_filters.append(reverse_filter) audio_filters.append(reverse_filter)
# 音量
vol = _clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
audio_filters.append(f"volume={vol:.4f}")
# aformat 归一化:统一输出格式为 48000Hz + stereo + fltp # aformat 归一化:统一输出格式为 48000Hz + stereo + fltp
audio_filters.append("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp") audio_filters.append("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp")
@@ -378,6 +435,11 @@ def concat_main_audio(
if reverse_filter: if reverse_filter:
audio_filters.append(reverse_filter) audio_filters.append(reverse_filter)
# 音量(0=静音,1=原声)
vol = _clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
audio_filters.append(f"volume={vol:.4f}")
# aformat 归一化:统一采样率48000Hz + 双声道stereo + fltp采样格式 # aformat 归一化:统一采样率48000Hz + 双声道stereo + fltp采样格式
# concat filter 要求所有输入音频参数完全一致,否则 exit=234 失败 # concat filter 要求所有输入音频参数完全一致,否则 exit=234 失败
audio_filters.append("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp") audio_filters.append("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp")
@@ -1,7 +1,8 @@
"""视频封面抽帧工具 — 从已渲染视频中抽取帧作为封面。 """视频封面抽帧工具 — 从视频中抽取帧作为封面,支持标题文字叠加
统一封面管道:视频渲染时标题已通过 ASS 字幕烧进视频, 统一封面管道:
渲染完成后直接从此视频抽帧,封面天然带标题,无需额外叠加逻辑 - 从已渲染视频抽帧:标题已通过 ASS 字幕烧进视频,帧天然带标题,无需再叠加
- 从源素材抽帧(API E2 兜底):源素材无标题,通过 Pillow 在帧上绘制标题文字。
""" """
from __future__ import annotations from __future__ import annotations
@@ -12,6 +13,40 @@ from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# ── 标题叠加(Pillow)──────────────────────────────────────────────────────
# 实现统一放在 packages/shared/title_overlay.pyAPI 和 Worker 共用。
def apply_title_overlay(
image_path: str,
title_text: str,
*,
color: str = "#ffffff",
position: str = "bottom",
font_size: int | None = None,
margin_ratio: float = 0.06,
stroke_width_ratio: float = 0.04,
) -> str:
"""在图片上绘制标题文字(指定颜色 + 黑色描边/阴影)。
委托给 packages.shared.title_overlay.apply_title_to_image
保持 Worker 内调用方式不变。title_text 为空时直接返回原路径。
"""
from packages.shared.title_overlay import apply_title_to_image
if not title_text or not title_text.strip():
return image_path
result = apply_title_to_image(
image_path,
title_text,
color=color,
position=position,
font_size=font_size,
margin_ratio=margin_ratio,
stroke_width_ratio=stroke_width_ratio,
)
return result or image_path
def extract_first_frame( def extract_first_frame(
video_path: str, video_path: str,
@@ -175,6 +210,9 @@ def extract_and_upload_cover_frames(
*, *,
num_frames: int = 3, num_frames: int = 3,
title_text: str = "", title_text: str = "",
title_color: str = "#ffffff",
title_position: str = "bottom",
title_font_size: int | None = None,
) -> list[dict]: ) -> list[dict]:
"""从视频中抽取多帧作为封面候选,上传到 OSS。 """从视频中抽取多帧作为封面候选,上传到 OSS。
@@ -182,7 +220,11 @@ def extract_and_upload_cover_frames(
video_path: 视频文件路径 video_path: 视频文件路径
plan_id: 编辑计划 ID(用于生成 storage key plan_id: 编辑计划 ID(用于生成 storage key
num_frames: 抽取帧数(默认 3 num_frames: 抽取帧数(默认 3
title_text: 标题文字(当前版本未叠加,预留参数) title_text: 标题文字;非空时用 Pillow 叠加到每帧。
从已渲染视频抽帧时通常传空(标题已烧录);从源素材抽帧时传标题。
title_color: 标题字体颜色(#RRGGBB
title_position: 标题位置 top/center/bottom
title_font_size: 标题字号,None 时自动计算
Returns: Returns:
封面候选列表,每项包含 {"url": str, "position": float} 封面候选列表,每项包含 {"url": str, "position": float}
@@ -208,6 +250,15 @@ def extract_and_upload_cover_frames(
seek_ratio=ratio, seek_ratio=ratio,
min_seek_seconds=0.5, min_seek_seconds=0.5,
) )
# 从源素材抽帧时叠加标题文字;已渲染视频标题已烧录时传空字符串跳过
if title_text and title_text.strip():
apply_title_overlay(
frame_path,
title_text,
color=title_color,
position=title_position,
font_size=title_font_size,
)
storage_key = f"covers/{plan_id}/frame_{i}.jpg" storage_key = f"covers/{plan_id}/frame_{i}.jpg"
url = upload_to_oss(frame_path, storage_key) url = upload_to_oss(frame_path, storage_key)
if url: if url:
@@ -36,6 +36,7 @@ from video_processing.ffmpeg_utils import (
DEFAULT_TRANSITION_DURATION, DEFAULT_TRANSITION_DURATION,
FFMPEG_BIN, FFMPEG_BIN,
probe_duration, probe_duration,
probe_has_audio,
probe_video_info, probe_video_info,
run_ffmpeg, run_ffmpeg,
) )
@@ -1000,6 +1001,11 @@ class UnifiedRenderService:
if abs(speed - 1.0) >= 1e-6: if abs(speed - 1.0) >= 1e-6:
return False, f"有调速: speed={speed:.2f}x" return False, f"有调速: speed={speed:.2f}x"
# 音量非默认(静音/放大)→ 需要音频滤镜重编码 → 不能 copy
vol = UnifiedRenderService._clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
return False, f"音量非默认: volume={vol:.2f}"
# 有倒放 → 需要重编码 → 不能 copy # 有倒放 → 需要重编码 → 不能 copy
reverse_config = ReverseConfig.from_dict(clip.config.get("reverse")) reverse_config = ReverseConfig.from_dict(clip.config.get("reverse"))
if reverse_config.enabled and (reverse_config.reverse_video or reverse_config.reverse_audio): if reverse_config.enabled and (reverse_config.reverse_video or reverse_config.reverse_audio):
@@ -1270,13 +1276,23 @@ class UnifiedRenderService:
"+faststart", "+faststart",
] ]
# 音频处理:background 通常是图片无音频,跳过;其他编码为 aac # 音频处理:background 通常是图片无音频,跳过;其他角色先探测是否真有音频流。
# background 以外的视频素材,默认带音频 # volume=0 不丢弃音频流,而是保留后通过 volume=0 滤镜静音,保持时间轴对齐。
has_audio = role != "background" clip_volume = UnifiedRenderService._clip_volume(clip)
if role != "background":
try:
has_audio = probe_has_audio(clip.local_path)
except Exception as e:
# probe_has_audio 内部已保守返回 True;只有极端错误才会到这里。
# 此时不静默丢音频,记录 error 并向上抛出,让任务失败而不是产出无声视频。
logger.error("[unified-render] 探测音频流发生致命错误,终止渲染: %s: %s", clip.local_path, e)
raise
else:
has_audio = False
if has_audio: if has_audio:
af_parts: list[str] = [] af_parts: list[str] = []
# 音频降噪 # 音频降噪(最先处理:在原始信号上降噪效果最好)
try: try:
from video_processing.noise_reduction_engine import NoiseReductionConfig, NoiseReductionEngine from video_processing.noise_reduction_engine import NoiseReductionConfig, NoiseReductionEngine
@@ -1290,13 +1306,16 @@ class UnifiedRenderService:
except Exception as e: except Exception as e:
logger.warning("[unified-render] 直通模式音频降噪应用失败,跳过: %s", e) logger.warning("[unified-render] 直通模式音频降噪应用失败,跳过: %s", e)
# 音频调速(与视频setpts对应,保持音画同步 # 音频调速(在降噪之后、音量之前,与 render_audio.py concat 路径保持一致
# SpeedEngine.build_audio_filter 内部已实现多级 atempo 串联,
# 自动处理超出 [0.5, 2.0] 范围的速度(如 0.25x → atempo=0.5,atempo=0.5)。
speed = UnifiedRenderService._clip_speed(clip) speed = UnifiedRenderService._clip_speed(clip)
if abs(speed - 1.0) >= 1e-6: if abs(speed - 1.0) >= 1e-6:
try: try:
from video_processing.speed_engine import SpeedConfig, SpeedEngine from video_processing.speed_engine import SpeedConfig, SpeedEngine
speed_cfg = SpeedConfig(speed=speed) speed_cfg = SpeedConfig(speed=speed)
speed_cfg.clamp()
speed_engine = SpeedEngine() speed_engine = SpeedEngine()
af_parts.append(speed_engine.build_audio_filter(speed_cfg)) af_parts.append(speed_engine.build_audio_filter(speed_cfg))
except Exception as e: except Exception as e:
@@ -1309,6 +1328,10 @@ class UnifiedRenderService:
if af_filter: if af_filter:
af_parts.append(af_filter) af_parts.append(af_filter)
# 片段音量(最后应用:确保调速/倒放后的最终输出音量准确,与 concat 路径一致)
if abs(clip_volume - 1.0) >= 1e-6:
af_parts.append(f"volume={clip_volume:.4f}")
if af_parts: if af_parts:
command.extend(["-af", ",".join(af_parts)]) command.extend(["-af", ",".join(af_parts)])
@@ -1976,6 +1999,16 @@ class UnifiedRenderService:
""" """
return _clip_playback_speed_pure(getattr(clip, "playback_speed", 1.0)) return _clip_playback_speed_pure(getattr(clip, "playback_speed", 1.0))
@staticmethod
def _clip_volume(clip: ResolvedClip) -> float:
"""获取 clip 的音量(config.volume)。缺省 1.0 原声,0.0 静音。"""
cfg = getattr(clip, "config", None) or {}
try:
vol = float(cfg.get("volume", 1.0))
except (TypeError, ValueError):
return 1.0
return max(0.0, vol)
@staticmethod @staticmethod
def _clip_adjusted_duration(clip: ResolvedClip) -> float: def _clip_adjusted_duration(clip: ResolvedClip) -> float:
"""计算调速后的 clip 实际时长(用于拼接计算)。 """计算调速后的 clip 实际时长(用于拼接计算)。
+10 -2
View File
@@ -14,9 +14,17 @@ celery_app.conf.imports = (
"worker_app.tasks.voice_extraction", "worker_app.tasks.voice_extraction",
"worker_app.tasks.voice_clone", "worker_app.tasks.voice_clone",
"worker_app.tasks.tts_synthesis", "worker_app.tasks.tts_synthesis",
"worker_app.tasks.edit_plan_generation",
"worker_app.tasks.compose_video",
"worker_app.tasks.batch_download", "worker_app.tasks.batch_download",
"worker_app.tasks._startup", "worker_app.tasks._startup",
"apps.worker.video_processing.dedup", "apps.worker.video_processing.dedup",
"worker_app.tasks.cleanup",
) )
# Celery Beat 定时任务调度
celery_app.conf.beat_schedule = {
"cleanup-stale-pending-tasks": {
"task": "worker.cleanup_stale_pending_tasks",
"schedule": 600.0, # 每 10 分钟(秒)
"options": {"expires": 300}, # 5 分钟过期,避免堆积
},
}
-5
View File
@@ -25,10 +25,6 @@ def __getattr__(name: str):
from .voice_extraction import extract_voice_task from .voice_extraction import extract_voice_task
return extract_voice_task return extract_voice_task
elif name == "compose_video":
from .compose_video import compose_video
return compose_video
elif name == "extract_background_task": elif name == "extract_background_task":
from .voice_extraction import extract_background_task from .voice_extraction import extract_background_task
@@ -58,7 +54,6 @@ def __getattr__(name: str):
__all__ = [ __all__ = [
"classify_asset", "classify_asset",
"compose_video",
"generate_video", "generate_video",
"healthcheck", "healthcheck",
"ingest_asset", "ingest_asset",
+40 -3
View File
@@ -10,6 +10,9 @@ logger = logging.getLogger(__name__)
# 孤儿任务超时阈值:渲染任务超过此时间未更新则视为卡死 # 孤儿任务超时阈值:渲染任务超过此时间未更新则视为卡死
ORPHAN_TASK_TIMEOUT_MINUTES = 10 ORPHAN_TASK_TIMEOUT_MINUTES = 10
# Pending 任务超时阈值:pending 任务在队列中等待超过此时间则自动清理
PENDING_TASK_TIMEOUT_MINUTES = 30
def cleanup_orphan_tasks(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> int: # pragma: no cover def cleanup_orphan_tasks(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> int: # pragma: no cover
"""清理数据库中超时未更新的 running GenerationTask(孤儿任务)。 """清理数据库中超时未更新的 running GenerationTask(孤儿任务)。
@@ -83,6 +86,38 @@ def cleanup_stale_jobs(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> in
return 0 return 0
def cleanup_stale_pending_tasks(timeout_minutes: int = PENDING_TASK_TIMEOUT_MINUTES) -> int: # pragma: no cover
"""清理数据库中卡在 pending 状态超时的 GenerationTask。
全局任务队列有 pending 数量上限,长期卡在 pending 的任务会占满队列,
导致新用户无法创建任务。通过 created_at 超时判断并标记为 failed。
Args:
timeout_minutes: 超时时间(分钟),默认 PENDING_TASK_TIMEOUT_MINUTES
Returns:
清理的任务数量
"""
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
SQLAlchemyGenerationTaskRepository,
)
session = SessionLocal()
try:
repo = SQLAlchemyGenerationTaskRepository(session)
count = repo.cleanup_stale_pending(timeout_minutes)
if count > 0:
logger.warning("清理了 %d 个超时的 pending GenerationTask(超过 %d 分钟未处理)", count, timeout_minutes)
else:
logger.info("无超时 pending GenerationTask 需要清理")
return count
except Exception as e:
logger.error("清理超时 pending GenerationTask 失败: %s", e, exc_info=True)
return 0
finally:
session.close()
def cleanup_all_stale_tasks(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> dict: # pragma: no cover def cleanup_all_stale_tasks(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES) -> dict: # pragma: no cover
"""统一清理所有超时的孤儿任务。 """统一清理所有超时的孤儿任务。
@@ -93,15 +128,17 @@ def cleanup_all_stale_tasks(timeout_minutes: int = ORPHAN_TASK_TIMEOUT_MINUTES)
""" """
gen_count = cleanup_orphan_tasks(timeout_minutes) gen_count = cleanup_orphan_tasks(timeout_minutes)
job_count = cleanup_stale_jobs(timeout_minutes) job_count = cleanup_stale_jobs(timeout_minutes)
total = gen_count + job_count pending_count = cleanup_stale_pending_tasks(PENDING_TASK_TIMEOUT_MINUTES)
total = gen_count + job_count + pending_count
if total > 0: if total > 0:
logger.warning( logger.warning(
"孤儿任务清理完成: GenerationTask=%d, Job=%d, 总计=%d", "任务清理完成: 孤儿 GenerationTask=%d, 孤儿 Job=%d, 超时 pending=%d, 总计=%d",
gen_count, gen_count,
job_count, job_count,
pending_count,
total, total,
) )
return {"generation_tasks": gen_count, "jobs": job_count} return {"generation_tasks": gen_count, "jobs": job_count, "pending": pending_count}
@worker_ready.connect @worker_ready.connect
+35
View File
@@ -0,0 +1,35 @@
"""定期清理任务 — Celery Beat 调度。
包含:
- cleanup_stale_pending_tasks: 定期清理卡在 pending 超时的 generation_tasks
"""
import logging
from celery import shared_task
from worker_app.tasks._startup import (
PENDING_TASK_TIMEOUT_MINUTES,
cleanup_stale_pending_tasks,
)
logger = logging.getLogger(__name__)
@shared_task(name="worker.cleanup_stale_pending_tasks")
def scheduled_cleanup_stale_pending(timeout_minutes: int = PENDING_TASK_TIMEOUT_MINUTES) -> dict:
"""Celery Beat 调度的定期任务:清理超时的 pending 任务。
每 10 分钟执行一次(由 celery_app.py 的 beat_schedule 配置),
查找所有 status='pending' 且 created_at < NOW() - timeout_minutes
的 generation_tasks,批量更新为 failed。
Args:
timeout_minutes: 超时时间(分钟),默认 30 分钟
Returns:
{"cleaned": int}
"""
count = cleanup_stale_pending_tasks(timeout_minutes)
if count > 0:
logger.info("[Beat] 清理了 %d 个超时 pending 任务(超时阈值 %d 分钟)", count, timeout_minutes)
return {"cleaned": count}
@@ -1,198 +0,0 @@
"""视频合成 Celery 任务 — Phase 8 任务 2.10.
使用 JobService 管理任务生命周期,通过 RenderAdapter 调用 UnifiedRenderService 执行合成。
"""
from __future__ import annotations
import os
import subprocess # pragma: no cover
import tempfile
from pathlib import Path
from celery.exceptions import SoftTimeLimitExceeded # pragma: no cover
from celery.utils.log import get_task_logger
from worker_app.celery_app import celery_app
from worker_app.db import SessionLocal
logger = get_task_logger(__name__)
# 任务超时时间(秒):超过此时间 Celery 会抛出 SoftTimeLimitExceeded
RENDER_TASK_SOFT_TIME_LIMIT = 600 # pragma: no cover # 10 分钟
# 硬超时:超过此时间进程会被强制 kill
RENDER_TASK_TIME_LIMIT = 660 # pragma: no cover # 10 分钟 + 1 分钟清理缓冲
def _get_job_service():
"""延迟导入 JobService,避免循环依赖。"""
from apps.api.app.services.job_service import JobService
from packages.adapters.sqlalchemy_impl.job_repository import SQLAlchemyJobRepository
db = SessionLocal()
repo = SQLAlchemyJobRepository(db)
return JobService(repo), db
@celery_app.task(
name="worker.compose_video",
bind=True,
max_retries=3,
default_retry_delay=60,
soft_time_limit=RENDER_TASK_SOFT_TIME_LIMIT,
time_limit=RENDER_TASK_TIME_LIMIT,
)
def compose_video(self, job_id: str, **kwargs): # pragma: no cover
"""视频合成任务。
使用 UnifiedRenderService(图层架构)进行渲染。
Args:
job_id: JobService 中的任务 ID
**kwargs: 来自 Job.payload 的额外参数(plan_id, output_path 等)
"""
job_service, db = _get_job_service()
try:
job = job_service.get_job(job_id)
if job is None:
logger.error("Job not found: %s", job_id)
return {"status": "error", "message": f"Job {job_id} not found"}
plan_id = job.payload.get("plan_id", "")
if not plan_id:
job_service.fail_job(job_id, "Missing plan_id in job payload")
return {"status": "error", "message": "Missing plan_id"}
# 使用 unified 渲染引擎
return _compose_with_unified_engine(self, job_service, job, plan_id, db)
except SoftTimeLimitExceeded:
# Celery 软超时:任务执行超过 soft_time_limit
error_msg = f"渲染任务超时(超过 {RENDER_TASK_SOFT_TIME_LIMIT // 60} 分钟)"
logger.error("视频合成超时: job_id=%s", job_id)
try:
job_service.fail_job(job_id, error_msg)
except Exception:
logger.exception("更新 Job 超时失败状态时出错")
# 超时不重试
return {"status": "error", "message": error_msg, "error_type": "timeout"}
except subprocess.TimeoutExpired as exc:
# FFmpeg 子进程超时
error_msg = f"FFmpeg 渲染超时({exc.timeout}s"
logger.error("视频合成 FFmpeg 超时: job_id=%s timeout=%s", job_id, exc.timeout)
try:
job_service.fail_job(job_id, error_msg)
except Exception:
logger.exception("更新 Job 超时失败状态时出错")
# 超时不重试
return {"status": "error", "message": error_msg, "error_type": "ffmpeg_timeout"}
except self.retry_exc as exc:
logger.warning("视频合成重试中: job_id=%s, exc=%s", job_id, exc)
raise
except subprocess.CalledProcessError as exc:
# FFmpeg 执行失败,提取有意义的错误信息
from video_processing.video_validation import get_exit_code_message
exit_msg = get_exit_code_message(exc.returncode)
stderr_text = (exc.stderr or "").strip()
stderr_tail = stderr_text[-300:] if len(stderr_text) > 300 else stderr_text
error_msg = f"渲染失败: {exit_msg}"
if stderr_tail:
error_msg += f" | {stderr_tail[:200]}"
logger.error("视频合成 FFmpeg 失败: job_id=%s %s", job_id, exit_msg)
try:
job_service.fail_job(job_id, error_msg[:500])
except Exception:
logger.exception("更新 Job 失败状态时出错")
# FFmpeg 错误不重试(通常是素材或配置问题)
return {"status": "error", "message": error_msg, "error_type": "ffmpeg_error", "exit_code": exc.returncode}
except Exception as exc:
logger.exception("视频合成异常: job_id=%s", job_id)
try:
job_service.fail_job(job_id, str(exc)[:500])
except Exception:
logger.exception("更新 Job 失败状态时出错")
raise self.retry(exc=exc, countdown=60) from exc
finally:
db.close()
def _compose_with_unified_engine(task, job_service, job, plan_id: str, db) -> dict: # pragma: no cover
"""新引擎渲染路径(UnifiedRenderService + RenderAdapter)。"""
job_id = job.id
# 标记为 running
job_service.update_progress(job_id, progress=10.0, current_stage="初始化统一渲染引擎")
from video_processing.render_adapter import RenderAdapter
adapter = RenderAdapter(db)
# 校验合成条件
job_service.update_progress(job_id, progress=15.0, current_stage="校验合成条件")
valid, errors, warnings, ready_count, total_count = adapter.validate_plan(plan_id)
if not valid:
error_msg = "; ".join(errors)
job_service.fail_job(job_id, f"合成校验失败: {error_msg}")
return {"status": "error", "message": error_msg}
# 进度回调
def progress_cb(progress: float, stage: str) -> None:
try:
job_service.update_progress(job_id, progress=progress, current_stage=stage)
except Exception:
logger.exception("更新进度失败")
# 执行渲染
job_service.update_progress(job_id, progress=20.0, current_stage="开始渲染")
logger.info("统一渲染引擎开始: job_id=%s plan_id=%s", job_id, plan_id)
result = adapter.render_plan(
plan_id=plan_id,
job_id=job_id,
progress_cb=progress_cb,
)
if not result.success:
error_msg = f"渲染失败: {result.error_message}"
job_service.fail_job(job_id, error_msg[:500])
raise RuntimeError(result.error_message)
# 更新 Job 状态为完成
result_data = {
"plan_id": plan_id,
"output_path": str(result.output_path) if result.output_path else "",
"storage_key": f"rendered/{plan_id}/{job_id}.mp4",
"output_url": result.output_url,
"estimated_duration": result.duration,
"clip_count": result.clip_count,
"engine": "unified",
"width": result.width,
"height": result.height,
"file_size": result.file_size,
}
job_service.complete_job(job_id, result=result_data)
logger.info(
"视频合成完成(unified): job_id=%s plan_id=%s duration=%.2fs",
job_id,
plan_id,
result.duration,
)
return {"status": "completed", "job_id": job_id, "result": result_data}
def _cleanup_output(job_id: str) -> None:
"""清理临时输出文件。"""
try:
_output_dir = os.environ.get("VIDEO_OUTPUT_DIR", os.path.join(tempfile.gettempdir(), "video_output"))
output_path = os.path.join(_output_dir, f"{job_id}.mp4")
if Path(output_path).exists():
Path(output_path).unlink()
except Exception as e:
logger.warning(f"清理输出文件失败: {e}", exc_info=True)
@@ -1,452 +0,0 @@
"""剪辑计划渲染任务 — 使用 UnifiedRenderService 统一渲染引擎.
Celery 任务 worker.render_edit_plan:
1. 加载 EditPlan + EditPlanClips
2. 通过 RenderAdapter 调用 UnifiedRenderService 渲染
3. 下载各片段素材 + 渲染
4. 上传渲染结果到 OSS
5. 创建 GeneratedVideo 记录 + 查重
6. 更新 EditPlan / EditPlanClip 状态
7. 更新 GenerationTask 进度
"""
from __future__ import annotations
import logging
from datetime import datetime, timezone
from pathlib import Path
from worker_app.celery_app import celery_app
from worker_app.db import SessionLocal
logger = logging.getLogger(__name__)
OUTPUT_WIDTH = 1280
OUTPUT_HEIGHT = 720
OUTPUT_FPS = 25.0
# ── 共享工具模块导入 ──────────────────────────────────────────────────────────
from video_processing.dedup_helpers import create_video_record_and_dedup
# ── Repository imports (延迟导入避免循环依赖) ─────────────────────────────────
def _get_repos():
"""获取数据库仓储实例"""
from packages.adapters.sqlalchemy_impl import SQLAlchemyEditPlanRepository
from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import (
SQLAlchemyEditPlanClipRepository,
)
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
SQLAlchemyGenerationTaskRepository,
)
db = SessionLocal()
try:
plan_repo = SQLAlchemyEditPlanRepository(db)
clip_repo = SQLAlchemyEditPlanClipRepository(db)
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
yield plan_repo, clip_repo, gen_task_repo, db
finally:
db.close()
# ── Celery Task ───────────────────────────────────────────────────────────────
def _mark_plan_failed(plan_repo, plan_id, gen_task_repo, generation_task_id, error_msg: str):
"""统一的计划失败标记工具。"""
plan = plan_repo.get(plan_id)
if plan and plan.status.value == "rendering":
plan.mark_failed()
plan_repo.update(plan)
if generation_task_id:
gen_task = gen_task_repo.get(generation_task_id)
if gen_task and gen_task.status.value != "failed":
gen_task.status = "failed"
gen_task.error_message = error_msg
gen_task.completed_at = datetime.now(timezone.utc)
try:
gen_task.append_log(
stage="render_failed",
message=error_msg[:500],
level="ERROR",
)
except Exception:
pass
gen_task_repo.update(gen_task)
def _finalize_render_success(
plan,
plan_repo,
clip_repo,
gen_task_repo,
db,
plan_id: str,
output_url: str,
storage_key: str,
duration: float,
file_size: int,
width: int,
height: int,
rendered_clip_ids: list[str],
failed_clip_ids: list[str],
generation_task_id: str,
output_path: Path,
engine: str,
thumbnail_url: str = "",
cover_candidates: list[dict] | None = None,
) -> dict:
"""渲染成功后的统一收尾:查重 + 更新状态 + 返回结果。"""
# 创建 GeneratedVideo 记录 + 查重
project_id = plan.project_id or ""
batch_id = plan.config.get("batch_id", "")
mode = plan.config.get("mode", "edit_plan")
# 从 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:
create_video_record_and_dedup(
generation_task_id=generation_task_id,
project_id=project_id,
user_id=plan.created_by_user_id or "",
batch_id=batch_id,
file_url=output_url or "",
file_size=file_size,
duration=duration,
video_path=str(output_path),
mode=mode,
session=db,
width=width,
height=height,
fps=OUTPUT_FPS,
name=video_name,
thumbnail_url=thumbnail_url,
)
except Exception as dedup_err:
logger.warning("查重失败(不影响渲染结果): %s", dedup_err)
# 更新片段状态为 rendered
for clip_id in rendered_clip_ids:
clip = clip_repo.get(clip_id)
if clip and clip.status.value == "ready":
clip.mark_rendered()
clip_repo.update(clip)
# 更新 EditPlan 状态为 completed + 回写实际渲染时长 + 结果数
plan.config["rendered_url"] = output_url or ""
plan.config["rendered_storage_key"] = storage_key
if hasattr(plan, "total_duration") and duration > 0:
plan.total_duration = duration
plan.mark_completed()
plan_repo.update(plan)
# 更新 GenerationTask 状态为 completed
if generation_task_id:
gen_task = gen_task_repo.get(generation_task_id)
if gen_task:
gen_task.status = "completed"
gen_task.progress = 100.0
# 剪辑计划是多片段合成 1 个成片,result_count = 1
gen_task.result_count = 1
gen_task.append_log(
stage="render_complete",
message=f"渲染完成,输出时长 {duration:.1f}s",
level="INFO",
engine=engine,
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(
"剪辑计划渲染完成: plan_id=%s engine=%s rendered=%d failed=%d duration=%.1fs",
plan_id,
engine,
len(rendered_clip_ids),
len(failed_clip_ids),
duration,
)
return {
"status": "completed",
"plan_id": plan_id,
"rendered_count": len(rendered_clip_ids),
"failed_count": len(failed_clip_ids),
"output_url": output_url,
"duration": duration,
}
def _render_with_unified(
plan,
clips,
plan_id: str,
generation_task_id: str,
plan_repo,
clip_repo,
gen_task_repo,
db,
) -> dict:
"""统一渲染引擎路径(通过 RenderAdapter 调用 UnifiedRenderService)。
RenderAdapter 内部处理:素材下载、BGM 准备、ASR 自动字幕、渲染执行、OSS 上传。
本函数只负责:业务状态更新、查重、收尾。
"""
from video_processing.render_adapter import RenderAdapter
adapter = RenderAdapter(db)
# 进度回调:更新 GenerationTask 进度
def _progress_cb(progress: float, stage: str):
if not generation_task_id:
return
try:
gen_task = gen_task_repo.get(generation_task_id)
if gen_task:
# 映射到 30%~90% 区间(素材下载前已到 30%)
mapped_progress = 30.0 + progress * 0.6
gen_task.progress = min(mapped_progress, 95.0)
gen_task.append_log(
stage="render_progress",
message=stage,
level="INFO",
progress=mapped_progress,
)
gen_task_repo.update(gen_task)
except Exception:
pass
try:
result = adapter.render_plan(
plan_id=plan_id,
job_id=generation_task_id or plan_id,
progress_cb=_progress_cb,
)
except Exception as render_err:
logger.error("渲染失败(unified): %s%s", plan_id, render_err)
_mark_plan_failed(plan_repo, plan_id, gen_task_repo, generation_task_id, f"渲染失败: {render_err}")
return {"status": "error", "message": f"渲染失败: {render_err}"}
if not result.success:
full_error = result.error_message or "渲染失败"
if result.error_detail:
full_error = f"{full_error}\n--- stderr ---\n{result.error_detail}"
logger.error("渲染失败(unified): %s%s", plan_id, result.error_message)
_mark_plan_failed(plan_repo, plan_id, gen_task_repo, generation_task_id, full_error)
return {"status": "error", "message": result.error_message or "渲染失败"}
output_path = result.output_path or Path("")
output_url = result.output_url or ""
thumbnail_url = result.thumbnail_url or ""
# adapter 上传到 rendered/{plan_id}/{job_id}.mp4,从 URL 提取实际 key
# 不能用 output.mp4 硬编码,否则 cover 等下游通过 key 构造的 URL 指向不存在的文件
if output_url:
from packages.shared.storage import get_shared_storage_service
storage_key = get_shared_storage_service().normalize_storage_key(output_url)
else:
storage_key = f"rendered/{plan_id}/{generation_task_id or plan_id}.mp4"
# 用 adapter 返回的 clip 明细(以 adapter 的结果为准)
rendered_clip_ids = result.rendered_clip_ids or []
failed_clip_ids = result.failed_clip_ids or []
# 将封面候选帧写入 plan.config(供封面 API 直接使用,跳过 MediaKit 抽帧)
if result.cover_candidates:
plan_config = plan.config or {}
plan_config["cover_candidates"] = result.cover_candidates
plan.config = plan_config
logger.info(
"封面候选帧已写入 plan.config: plan_id=%s count=%d",
plan_id,
len(result.cover_candidates),
)
return _finalize_render_success(
plan=plan,
plan_repo=plan_repo,
clip_repo=clip_repo,
gen_task_repo=gen_task_repo,
db=db,
plan_id=plan_id,
output_url=output_url or "",
storage_key=storage_key,
duration=result.duration,
file_size=result.file_size,
width=result.width,
height=result.height,
rendered_clip_ids=rendered_clip_ids,
failed_clip_ids=failed_clip_ids,
generation_task_id=generation_task_id,
output_path=output_path,
engine="unified",
thumbnail_url=thumbnail_url,
cover_candidates=result.cover_candidates,
)
@celery_app.task(
name="worker.render_edit_plan",
bind=True,
max_retries=2,
soft_time_limit=600, # 10 分钟软超时
time_limit=660, # 11 分钟硬超时
)
def render_edit_plan(self, plan_id: str) -> dict: # pragma: no cover
"""渲染剪辑计划
流程:
1. 加载 EditPlan + EditPlanClips
2. 通过 RenderAdapter 调用 UnifiedRenderService 渲染
3. 下载素材 + 渲染
4. 上传渲染结果到 OSS
5. 创建 GeneratedVideo 记录 + 查重
6. 更新 EditPlan → completed, EditPlanClips → rendered
7. 更新 GenerationTask 进度
"""
logger.info("开始渲染剪辑计划: plan_id=%s", plan_id)
generation_task_id = ""
for repos in _get_repos():
plan_repo, clip_repo, gen_task_repo, db = repos
try:
# 1. 加载 EditPlan
plan = plan_repo.get(plan_id)
if plan is None:
logger.error("剪辑计划不存在: %s", plan_id)
return {"status": "error", "message": f"计划不存在: {plan_id}"}
# 获取 generation_task_id(提前读取,确保 except 块可用)
generation_task_id = plan.config.get("generation_task_id", "")
# 2. 准备渲染(使用 unified 渲染引擎)
# 3. 加载片段列表(按 order 排序)
clips = clip_repo.list_by_plan(plan_id, skip=0, limit=10000)
if not clips:
logger.warning("剪辑计划没有片段: %s", plan_id)
plan.mark_failed()
plan_repo.update(plan)
return {"status": "error", "message": "没有可渲染的片段"}
# 更新 GenerationTask 状态为 running
if generation_task_id:
gen_task = gen_task_repo.get(generation_task_id)
if gen_task:
gen_task.status = "running"
gen_task.started_at = datetime.now(timezone.utc)
gen_task.append_log(
stage="render_start",
message=f"开始渲染,片段数 {len(clips)}",
level="INFO",
engine="unified",
clip_count=len(clips),
)
gen_task_repo.update(gen_task)
# 3. 渲染前取消检查
if generation_task_id:
current_task = gen_task_repo.get(generation_task_id)
if current_task:
task_status = (
current_task.status.value if hasattr(current_task.status, "value") else str(current_task.status)
)
if task_status == "cancelled":
logger.info("任务已被取消,中止渲染: plan_id=%s task_id=%s", plan_id, generation_task_id)
if plan.status.value == "rendering":
try:
plan.resume_editing()
plan_repo.update(plan)
except ValueError:
pass
return {"status": "cancelled", "plan_id": plan_id, "message": "任务已取消"}
# 4. 渲染(unified 引擎:RenderAdapter 统一处理下载 + BGM + ASR + 渲染 + 上传)
result = _render_with_unified(
plan=plan,
clips=clips,
plan_id=plan_id,
generation_task_id=generation_task_id,
plan_repo=plan_repo,
clip_repo=clip_repo,
gen_task_repo=gen_task_repo,
db=db,
)
result["engine"] = "unified"
return result
except Exception as exc:
# 超时异常不重试,直接标记失败
from celery.exceptions import SoftTimeLimitExceeded
is_timeout = isinstance(exc, SoftTimeLimitExceeded)
if is_timeout:
logger.error("渲染剪辑计划超时: plan_id=%s", plan_id)
else:
logger.exception("渲染剪辑计划异常: %s", plan_id)
# 尝试标记计划和 GenerationTask 为失败
error_msg = "渲染任务超时(超过10分钟)" if is_timeout else f"渲染异常: {type(exc).__name__}: {exc}"
try:
plan = plan_repo.get(plan_id)
if plan and plan.status.value == "rendering":
plan.mark_failed()
plan_repo.update(plan)
except Exception as e:
logger.warning("标记计划失败时异常: plan_id=%s error=%s", plan_id, e, exc_info=True)
# 更新 GenerationTask 状态为 failed,前端轮询能看到失败状态
try:
if generation_task_id:
gen_task = gen_task_repo.get(generation_task_id)
if gen_task and gen_task.status.value != "failed":
gen_task.status = "failed"
gen_task.error_message = error_msg
gen_task.completed_at = datetime.now(timezone.utc)
try:
log_stage = "render_timeout" if is_timeout else "render_failed"
gen_task.append_log(
stage=log_stage,
message=error_msg[:500],
level="ERROR",
exception_type=type(exc).__name__,
)
except Exception:
pass
gen_task_repo.update(gen_task)
logger.info(
"GenerationTask 已标记为 failed: task_id=%s plan_id=%s",
generation_task_id,
plan_id,
)
except Exception as e:
logger.warning(
"更新 GenerationTask 失败状态时异常: task_id=%s error=%s", generation_task_id, e, exc_info=True
)
raise self.retry(exc=exc, countdown=60) from exc
return {"status": "error", "message": "数据库连接失败"}
File diff suppressed because it is too large Load Diff
+19875
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -17,6 +17,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libpq-dev \ libpq-dev \
libpq5 \ libpq5 \
ffmpeg \ ffmpeg \
fonts-noto-cjk \
fontconfig \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# 创建虚拟环境 # 创建虚拟环境
+5
View File
@@ -6,8 +6,13 @@ set -e
CONCURRENCY="${WORKER_CONCURRENCY:-2}" CONCURRENCY="${WORKER_CONCURRENCY:-2}"
# ⚠️ 部署约束:此 Worker 必须且只能运行单实例(replicas=1)
# -B 标志嵌入 celery beatbeat 负责定期触发 pending 超时清理等定时任务
# 多实例部署会导致每个 Worker 独立运行 Beat,造成定时任务重复执行
# 若需横向扩展 Worker,必须将 Beat 拆分为独立服务(celery beat -A worker_app.celery_app
exec celery \ exec celery \
-A worker_app.celery_app \ -A worker_app.celery_app \
worker \ worker \
--loglevel=info \ --loglevel=info \
"-B" \
"--concurrency=${CONCURRENCY}" "--concurrency=${CONCURRENCY}"
@@ -41,7 +41,7 @@ def _to_domain(model: GenerationTaskModel) -> GenerationTask:
output_width=getattr(model, "output_width", 1280) or 1280, output_width=getattr(model, "output_width", 1280) or 1280,
output_height=getattr(model, "output_height", 720) or 720, output_height=getattr(model, "output_height", 720) or 720,
cover_url=getattr(model, "cover_url", "") or "", cover_url=getattr(model, "cover_url", "") or "",
custom_title=getattr(model, "custom_title", "") or "", title_config=dict(getattr(model, "title_config", {}) or {}),
logs=model.logs or "[]", logs=model.logs or "[]",
created_at=model.created_at, created_at=model.created_at,
updated_at=model.updated_at, updated_at=model.updated_at,
@@ -85,7 +85,7 @@ class SQLAlchemyGenerationTaskRepository:
output_width=task.output_width, output_width=task.output_width,
output_height=task.output_height, output_height=task.output_height,
cover_url=task.cover_url or "", cover_url=task.cover_url or "",
custom_title=task.custom_title or "", title_config=dict(task.title_config) if task.title_config else {},
logs=task.logs, logs=task.logs,
created_at=task.created_at, created_at=task.created_at,
updated_at=task.updated_at, updated_at=task.updated_at,
@@ -272,7 +272,7 @@ class SQLAlchemyGenerationTaskRepository:
model.output_width = task.output_width model.output_width = task.output_width
model.output_height = task.output_height model.output_height = task.output_height
model.cover_url = task.cover_url or "" model.cover_url = task.cover_url or ""
model.custom_title = task.custom_title or "" model.title_config = dict(task.title_config) if task.title_config else {}
model.logs = task.logs model.logs = task.logs
self.session.commit() self.session.commit()
return task return task
@@ -310,3 +310,42 @@ class SQLAlchemyGenerationTaskRepository:
model.completed_at = datetime.now(timezone.utc) model.completed_at = datetime.now(timezone.utc)
self.session.commit() self.session.commit()
return len(models) return len(models)
def cleanup_stale_pending(self, timeout_minutes: int = 30) -> int:
"""清理超时的 pending 任务(未被 Worker 拉取的任务)。
全局任务队列有 pending 数量上限,长期卡在 pending 的任务会占满队列,
导致新用户无法创建任务。将超时的 pending 任务标记为 failed。
Args:
timeout_minutes: 超时时间(分钟),默认 30 分钟
Returns:
清理的任务数量
"""
from datetime import timedelta
cutoff = datetime.now(timezone.utc) - timedelta(minutes=timeout_minutes)
error_info = {
"error_type": "PendingTimeout",
"message": f"任务在 pending 状态停留超过 {timeout_minutes} 分钟,自动清理",
"failed_at": datetime.now(timezone.utc).isoformat(),
}
count = (
self.session.query(GenerationTaskModel)
.filter(
GenerationTaskModel.status == GenerationTaskStatus.PENDING.value,
GenerationTaskModel.created_at < cutoff,
)
.update(
{
GenerationTaskModel.status: GenerationTaskStatus.FAILED.value,
GenerationTaskModel.error_message: "pending timeout: auto cleanup",
GenerationTaskModel.error_info: error_info,
GenerationTaskModel.completed_at: datetime.now(timezone.utc),
},
synchronize_session=False,
)
)
self.session.commit()
return count
@@ -298,6 +298,7 @@ class GenerationTaskModel(Base):
output_height = Column(Integer, nullable=False, default=720) output_height = Column(Integer, nullable=False, default=720)
cover_url = Column(String(1000), nullable=False, default="") cover_url = Column(String(1000), nullable=False, default="")
custom_title = Column(String(500), nullable=False, default="") custom_title = Column(String(500), nullable=False, default="")
title_config = Column(JSON, nullable=False, default=dict)
bgm_config = Column(JSON, nullable=False, default=dict) bgm_config = Column(JSON, nullable=False, default=dict)
extra_meta = Column("metadata", JSON, nullable=False, default=dict) extra_meta = Column("metadata", JSON, nullable=False, default=dict)
logs = Column(Text, nullable=False, default="[]", server_default="[]") logs = Column(Text, nullable=False, default="[]", server_default="[]")
+1 -2
View File
@@ -31,7 +31,6 @@ class CreateGenerationTaskCommand:
output_width: int = 1280 output_width: int = 1280
output_height: int = 720 output_height: int = 720
cover_url: str = "" cover_url: str = ""
custom_title: str = ""
title_config: dict = field(default_factory=dict) title_config: dict = field(default_factory=dict)
@@ -68,7 +67,7 @@ class CreateGenerationTaskUseCase:
output_width=command.output_width, output_width=command.output_width,
output_height=command.output_height, output_height=command.output_height,
cover_url=command.cover_url, cover_url=command.cover_url,
custom_title=command.custom_title, title_config=command.title_config,
) )
return self.generation_task_repository.create(task) return self.generation_task_repository.create(task)
+6 -6
View File
@@ -120,7 +120,7 @@ class GenerationTask:
output_width: int = 1280 output_width: int = 1280
output_height: int = 720 output_height: int = 720
cover_url: str = "" cover_url: str = ""
custom_title: str = "" title_config: dict = field(default_factory=dict)
extra_meta: dict = field(default_factory=dict) extra_meta: dict = field(default_factory=dict)
logs: str = "[]" logs: str = "[]"
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
@@ -152,7 +152,7 @@ class GenerationTask:
output_width: int = 1280, output_width: int = 1280,
output_height: int = 720, output_height: int = 720,
cover_url: str = "", cover_url: str = "",
custom_title: str = "", title_config: dict | None = None,
extra_meta: dict | None = None, extra_meta: dict | None = None,
) -> "GenerationTask": ) -> "GenerationTask":
if not project_id.strip() and not template_id.strip(): if not project_id.strip() and not template_id.strip():
@@ -183,7 +183,7 @@ class GenerationTask:
output_width=output_width, output_width=output_width,
output_height=output_height, output_height=output_height,
cover_url=cover_url, cover_url=cover_url,
custom_title=custom_title, title_config=dict(title_config) if title_config else {},
extra_meta=dict(extra_meta) if extra_meta else {}, extra_meta=dict(extra_meta) if extra_meta else {},
) )
@@ -303,10 +303,10 @@ class GenerationTask:
self, self,
*, *,
cover_url: str = "", cover_url: str = "",
custom_title: str = "",
extra_meta: dict | None = None, extra_meta: dict | None = None,
output_width: int = 0, output_width: int = 0,
output_height: int = 0, output_height: int = 0,
title_config: dict | None = None,
) -> None: ) -> None:
"""将预览任务确认为正式产出。 """将预览任务确认为正式产出。
@@ -316,12 +316,12 @@ class GenerationTask:
self.is_preview = False self.is_preview = False
if cover_url: if cover_url:
self.cover_url = cover_url self.cover_url = cover_url
if custom_title:
self.custom_title = custom_title
if output_width > 0: if output_width > 0:
self.output_width = output_width self.output_width = output_width
if output_height > 0: if output_height > 0:
self.output_height = output_height self.output_height = output_height
if title_config:
self.title_config = dict(title_config)
if extra_meta: if extra_meta:
self.extra_meta.update(extra_meta) self.extra_meta.update(extra_meta)
self.updated_at = datetime.now(timezone.utc) self.updated_at = datetime.now(timezone.utc)
+1 -1
View File
@@ -364,7 +364,7 @@ class MediaKitClient:
data = response.json() data = response.json()
status = data.get("status") status = data.get("status")
if status == "success": if status in ("completed", "success"):
result = data.get("result", {}) result = data.get("result", {})
snapshots = result.get("snapshots", []) snapshots = result.get("snapshots", [])
logger.info( logger.info(
+171
View File
@@ -0,0 +1,171 @@
"""封面标题文字叠加(Pillow)— API / Worker 共用。
在封面帧上绘制白色标题文字 + 黑色描边/阴影,支持 CJK 字体和自动换行。
从已渲染视频抽帧时通常不需要调用(标题已烧录);
从源素材抽帧(API E2 兜底)时调用,保证封面带标题。
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
# 按优先级查找 CJK 字体(Debian/Ubuntu fonts-noto-cjk 安装路径)
_FONT_CANDIDATES = (
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
)
def find_title_font(size: int):
"""查找可用的 CJK 字体并返回 PIL ImageFont,找不到返回 None。"""
try:
from PIL import ImageFont
except ImportError:
return None
for fp in _FONT_CANDIDATES:
if Path(fp).exists():
try:
return ImageFont.truetype(fp, size=size)
except Exception:
continue
logger.warning("未找到 CJK 字体,标题叠加将使用 PIL 默认字体(中文可能显示为方块)")
return ImageFont.load_default()
def _parse_hex_color(color: str, fallback=(255, 255, 255)) -> tuple[int, int, int]:
"将 #RRGGBB / #RGB 解析为 RGB 元组,失败返回 fallback。"
if not color or not isinstance(color, str):
return fallback
c = color.strip().lstrip("#")
try:
if len(c) == 6:
return (int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16))
if len(c) == 3:
return (int(c[0] * 2, 16), int(c[1] * 2, 16), int(c[2] * 2, 16))
except (ValueError, IndexError):
pass
return fallback
def wrap_title_text(text: str, font, max_width: int) -> list[str]:
"""按像素宽度对中英文混合文本自动换行,支持显式 \\n。"""
lines: list[str] = []
current = ""
for ch in text:
if ch == "\n":
if current:
lines.append(current)
current = ""
continue
trial = current + ch
try:
bbox = font.getbbox(trial)
width = bbox[2] - bbox[0]
except Exception:
width = len(trial) * (font.size // 2)
if width <= max_width:
current = trial
else:
if current:
lines.append(current)
current = ch
if current:
lines.append(current)
return lines
def apply_title_to_image(
image_path: str,
title_text: str,
*,
color: str = "#ffffff",
position: str = "bottom",
font_size: Optional[int] = None,
margin_ratio: float = 0.06,
stroke_width_ratio: float = 0.04,
) -> Optional[str]:
"""在图片上绘制标题文字并覆盖保存。
Args:
image_path: 图片路径(处理结果覆盖写回)
title_text: 标题文字;为空直接返回 None 表示跳过
color: 字体颜色(#RRGGBB),默认白色
position: top / center / bottom
font_size: 字号,None 时按图片宽度自动计算
margin_ratio: 边缘留白占短边比例
stroke_width_ratio: 描边宽度占字号比例
Returns:
成功返回 image_path;标题为空或 PIL 不可用返回 None。
"""
if not title_text or not title_text.strip():
return None
try:
from PIL import Image, ImageDraw
except ImportError:
logger.warning("Pillow 未安装,跳过标题叠加: image=%s", image_path)
return None
img = Image.open(image_path).convert("RGB")
draw = ImageDraw.Draw(img)
img_w, img_h = img.size
if font_size is None:
font_size = max(28, min(72, img_w // 16))
font = find_title_font(font_size)
if font is None:
return None
text_rgb = _parse_hex_color(color)
stroke_width = max(2, int(font_size * stroke_width_ratio))
margin = int(min(img_w, img_h) * margin_ratio)
max_text_width = img_w - 2 * margin
lines = wrap_title_text(title_text.strip(), font, max_text_width)
if not lines:
return None
line_heights = []
for ln in lines:
bbox = font.getbbox(ln)
line_heights.append(bbox[3] - bbox[1])
line_height = max(line_heights) if line_heights else font_size
line_gap = int(line_height * 0.3)
total_height = len(lines) * line_height + (len(lines) - 1) * line_gap
if position == "top":
y_start = margin
elif position == "center":
y_start = (img_h - total_height) // 2
else:
y_start = img_h - total_height - margin
for i, ln in enumerate(lines):
bbox = font.getbbox(ln)
line_w = bbox[2] - bbox[0]
x = (img_w - line_w) // 2
y = y_start + i * (line_height + line_gap)
# 阴影
draw.text((x + 2, y + 2), ln, font=font, fill=(0, 0, 0))
# 文字(颜色由 color 参数控制)+ 黑色描边
draw.text(
(x, y),
ln,
font=font,
fill=text_rgb,
stroke_width=stroke_width,
stroke_fill=(0, 0, 0),
)
img.save(image_path, "JPEG", quality=92)
return image_path
+1
View File
@@ -29,3 +29,4 @@ httpx==0.27.2
# Prometheus monitoring # Prometheus monitoring
prometheus-client==0.21.1 prometheus-client==0.21.1
Pillow==10.4.0
@@ -1,234 +0,0 @@
"""剪辑模式渲染集成测试.
验证剪辑模式(ONE_TAKE / VOICE_OVER)通过
_build_plan_and_clips_from_task + UnifiedRenderService 的完整渲染流程。
注:PIP / VOICE_PIP 已下线,统一映射为 ONE_TAKE。
需要 ffmpeg 可用;CI 无 ffmpeg 时自动跳过。
"""
from __future__ import annotations
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
from video_processing.unified_render_service import (
RenderResult,
UnifiedRenderService,
_resolve_layer_role,
)
from worker_app.tasks.generation import _build_plan_and_clips_from_task
pytestmark = pytest.mark.skipif(
not shutil.which("ffmpeg"),
reason="ffmpeg not available",
)
# ── 辅助函数 ──────────────────────────────────────────────────────────────────
def _generate_test_video(path: Path, duration: float = 3.0, color: str = "red") -> None:
"""生成一个纯色测试视频。"""
cmd = [
"ffmpeg",
"-y",
"-f",
"lavfi",
"-i",
f"color=c={color}:s=640x360:d={duration}:r=25",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-movflags",
"+faststart",
str(path),
]
subprocess.run(cmd, check=True, capture_output=True, timeout=30)
def _render_with_mode(
mode: str,
num_clips: int = 3,
duration: float = 2.0,
) -> tuple[RenderResult, Path]:
"""用指定模式生成测试视频并渲染,返回 (result, work_dir)。
调用方负责清理 work_dir。
"""
work_dir = Path(tempfile.mkdtemp(prefix="test_4mode_"))
# 生成测试视频素材
colors = ["red", "green", "blue", "yellow", "purple"]
downloaded_paths: list[Path] = []
for i in range(num_clips):
p = work_dir / f"test_{i:03d}.mp4"
_generate_test_video(p, duration=duration, color=colors[i % len(colors)])
downloaded_paths.append(p)
# 构建虚拟 plan + clips
task_id = f"test_task_{mode}"
plan, clips, asset_path_map = _build_plan_and_clips_from_task(
task_id=task_id,
downloaded_paths=downloaded_paths,
mode=mode,
)
# 渲染
service = UnifiedRenderService(
plan=plan,
clips=clips,
asset_path_map=asset_path_map,
work_dir=work_dir,
output_width=640,
output_height=360,
output_fps=25,
)
result = service.render()
return result, work_dir
# ── 测试 _build_plan_and_clips_from_task ──────────────────────────────────────
class TestBuildPlanAndClips:
"""测试 4 种模式的虚拟 plan 构建。"""
def _make_paths(self, n: int) -> list[Path]:
return [Path(f"/tmp/test_{i}.mp4") for i in range(n)]
def test_one_take_mode(self):
paths = self._make_paths(3)
plan, clips, asset_map = _build_plan_and_clips_from_task("t1", paths, "one_take")
assert plan.id == "t1"
assert len(clips) == 3
assert all(c.clip_type == "main" for c in clips)
assert len(asset_map) == 3
def test_pip_mode_maps_to_one_take(self):
"""PIP 已下线,映射为 one_take → 全部 main clips。"""
paths = self._make_paths(3)
plan, clips, asset_map = _build_plan_and_clips_from_task("t2", paths, "pip")
assert len(clips) == 3
assert all(c.clip_type == "main" for c in clips)
def test_voice_over_mode(self):
paths = self._make_paths(3)
plan, clips, asset_map = _build_plan_and_clips_from_task("t3", paths, "voice_over")
assert len(clips) == 3
assert all(c.clip_type == "main" for c in clips)
assert all(c.config.get("role") == "b_roll" for c in clips)
def test_voice_pip_mode_maps_to_one_take(self):
"""VOICE_PIP 已下线,映射为 one_take → 全部 main clips。"""
paths = self._make_paths(4)
plan, clips, asset_map = _build_plan_and_clips_from_task("t4", paths, "voice_pip")
assert len(clips) == 4
assert all(c.clip_type == "main" for c in clips)
def test_unknown_mode_defaults_to_one_take(self):
paths = self._make_paths(2)
plan, clips, asset_map = _build_plan_and_clips_from_task("t5", paths, "unknown_mode")
assert len(clips) == 2
assert all(c.clip_type == "main" for c in clips)
def test_asset_path_map_keys_match_clip_asset_ids(self):
paths = self._make_paths(3)
_, clips, asset_map = _build_plan_and_clips_from_task("t6", paths, "one_take")
clip_asset_ids = {c.asset_id for c in clips}
map_keys = set(asset_map.keys())
assert clip_asset_ids == map_keys
# ── 测试图层分组(4 模式) ────────────────────────────────────────────────────
class TestFourModeLayerGrouping:
"""验证 4 种模式的 clip_type 分布经 _resolve_layer_role 后产生正确的图层。"""
def test_one_take_layers(self):
"""ONE_TAKE: 3 main → 1 main layer。"""
paths = [Path(f"/tmp/ot_{i}.mp4") for i in range(3)]
_, clips, _ = _build_plan_and_clips_from_task("ot", paths, "one_take")
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"main"}
def test_pip_layers_now_one_take(self):
"""PIP 已下线 → one_take: 3 main → 1 main layer。"""
paths = [Path(f"/tmp/pip_{i}.mp4") for i in range(3)]
_, clips, _ = _build_plan_and_clips_from_task("pip", paths, "pip")
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"main"}
def test_voice_over_layers(self):
"""VOICE_OVER: 3 main(b_roll) → broll。"""
paths = [Path(f"/tmp/vo_{i}.mp4") for i in range(3)]
_, clips, _ = _build_plan_and_clips_from_task("vo", paths, "voice_over")
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"broll"}
def test_voice_pip_layers_now_one_take(self):
"""VOICE_PIP 已下线 → one_take: 4 main → main layer。"""
paths = [Path(f"/tmp/vpip_{i}.mp4") for i in range(4)]
_, clips, _ = _build_plan_and_clips_from_task("vpip", paths, "voice_pip")
roles = {_resolve_layer_role(c.clip_type, c.config) for c in clips}
assert roles == {"main"}
# ── 端到端渲染测试(需要 ffmpeg) ─────────────────────────────────────────────
class TestEndToEndRendering:
"""4 种模式的完整渲染测试,验证输出文件存在且时长合理。"""
def test_one_take_render(self):
result, work_dir = _render_with_mode("one_take", num_clips=2, duration=2.0)
try:
assert result.output_path.exists()
assert result.file_size > 0
assert result.duration > 0
assert result.width == 640
assert result.height == 360
finally:
shutil.rmtree(work_dir, ignore_errors=True)
def test_pip_render(self):
result, work_dir = _render_with_mode("pip", num_clips=2, duration=2.0)
try:
assert result.output_path.exists()
assert result.file_size > 0
assert result.duration > 0
finally:
shutil.rmtree(work_dir, ignore_errors=True)
def test_voice_over_render(self):
result, work_dir = _render_with_mode("voice_over", num_clips=2, duration=2.0)
try:
assert result.output_path.exists()
assert result.file_size > 0
assert result.duration > 0
finally:
shutil.rmtree(work_dir, ignore_errors=True)
def test_voice_pip_render(self):
result, work_dir = _render_with_mode("voice_pip", num_clips=3, duration=2.0)
try:
assert result.output_path.exists()
assert result.file_size > 0
assert result.duration > 0
finally:
shutil.rmtree(work_dir, ignore_errors=True)
-189
View File
@@ -1,189 +0,0 @@
"""全链路集成测试.
验证 PlanGeneratorService → UnifiedRenderService → 查重 的端到端流程。
"""
from __future__ import annotations
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
from video_processing.unified_render_service import (
UnifiedRenderService,
)
from worker_app.tasks.generation import _build_plan_and_clips_from_task
pytestmark = pytest.mark.skipif(
not shutil.which("ffmpeg"),
reason="ffmpeg not available",
)
def _generate_test_video(path: Path, duration: float = 3.0) -> None:
"""生成一个测试视频。"""
cmd = [
"ffmpeg",
"-y",
"-f",
"lavfi",
"-i",
f"color=c=blue:s=640x360:d={duration}:r=25",
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
str(path),
]
subprocess.run(cmd, check=True, capture_output=True, timeout=30)
def _generate_test_audio(path: Path, duration: float = 5.0) -> None:
"""生成一个测试音频文件。"""
cmd = [
"ffmpeg",
"-y",
"-f",
"lavfi",
"-i",
f"sine=frequency=440:duration={duration}",
"-c:a",
"aac",
"-b:a",
"128k",
str(path),
]
subprocess.run(cmd, check=True, capture_output=True, timeout=30)
class TestFullPipeline:
"""验证从虚拟 plan 构建到渲染输出的完整流程。"""
def test_one_take_pipeline(self):
"""ONE_TAKE 模式完整流程。"""
with tempfile.TemporaryDirectory() as tmpdir:
work_dir = Path(tmpdir)
# 生成测试素材
paths = []
for i in range(3):
p = work_dir / f"clip_{i}.mp4"
_generate_test_video(p, duration=2.0)
paths.append(p)
# 构建虚拟 plan
plan, clips, asset_map = _build_plan_and_clips_from_task("pipeline_test", paths, "one_take")
# 渲染
service = UnifiedRenderService(
plan=plan,
clips=clips,
asset_path_map=asset_map,
work_dir=work_dir,
output_width=640,
output_height=360,
)
result = service.render()
assert result.output_path.exists()
assert result.duration > 0
assert result.file_size > 0
assert result.width == 640
assert result.height == 360
def test_pipeline_with_audio_mux(self):
"""渲染 + 混音后处理。"""
with tempfile.TemporaryDirectory() as tmpdir:
work_dir = Path(tmpdir)
# 生成测试素材
video_path = work_dir / "clip_0.mp4"
_generate_test_video(video_path, duration=3.0)
# 构建虚拟 plan
plan, clips, asset_map = _build_plan_and_clips_from_task("audio_test", [video_path], "one_take")
# 渲染
service = UnifiedRenderService(
plan=plan,
clips=clips,
asset_path_map=asset_map,
work_dir=work_dir,
output_width=640,
output_height=360,
)
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_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
def test_single_clip_pipeline(self):
"""单 clip 渲染(无转场)。"""
with tempfile.TemporaryDirectory() as tmpdir:
work_dir = Path(tmpdir)
video_path = work_dir / "single.mp4"
_generate_test_video(video_path, duration=5.0)
plan, clips, asset_map = _build_plan_and_clips_from_task("single_test", [video_path], "one_take")
service = UnifiedRenderService(
plan=plan,
clips=clips,
asset_path_map=asset_map,
work_dir=work_dir,
output_width=640,
output_height=360,
)
result = service.render()
assert result.output_path.exists()
assert result.duration > 0
def test_dedup_helper_integration(self):
"""验证 dedup_helpers.create_video_record_and_dedup 的导入和签名。"""
# 只验证函数存在且签名正确(不实际调用,需要数据库)
import inspect
from video_processing.dedup_helpers import create_video_record_and_dedup
sig = inspect.signature(create_video_record_and_dedup)
params = set(sig.parameters.keys())
expected = {
"generation_task_id",
"project_id",
"batch_id",
"file_url",
"file_size",
"duration",
"video_path",
"mode",
"session",
"width",
"height",
"fps",
}
assert expected.issubset(params), f"Missing params: {expected - params}"
-8
View File
@@ -149,14 +149,6 @@ class TestWorkerGenerationNoPreviewOverride:
assert 'resolution = "854x480"' not in source, "Should not override resolution to 480p in preview mode" assert 'resolution = "854x480"' not in source, "Should not override resolution to 480p in preview mode"
assert 'bitrate = "1M"' not in source, "Should not override bitrate to 1M in preview mode" assert 'bitrate = "1M"' not in source, "Should not override bitrate to 1M in preview mode"
def test_parallel_download_still_works(self):
"""并行下载逻辑保留。"""
with open("apps/worker/worker_app/tasks/generation.py") as f:
source = f.read()
assert "ThreadPoolExecutor" in source, "Should use ThreadPoolExecutor for parallel downloads"
assert "as_completed" in source, "Should use as_completed for result collection"
# ── 5. generation_preview.py 不再有 PREVIEW_RESOLUTION ── # ── 5. generation_preview.py 不再有 PREVIEW_RESOLUTION ──

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