Compare commits

...

60 Commits

Author SHA1 Message Date
saas-backend-agent 277ff5428e fix: 修 test_rollback_value_error_400 pre-existing failure
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
AI Code Review / AI Code Review (pull_request) Successful in 6m43s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Failing after 11m6s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m17s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m47s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 11s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m18s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 9m52s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 10m1s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 191h34m6s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 191h34m11s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 191h34m11s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 191h34m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 191h44m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 191h44m17s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 191h54m33s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 191h54m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 191h55m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 191h55m5s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 191h55m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 191h55m11s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 191h55m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 191h55m15s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 191h55m26s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 192h8m50s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 192h29m17s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 192h29m50s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 192h29m59s
endpoint 对 ValueError 返回 404(版本不存在=Not Found),
测试断言写的 400 是错的,改为 404 与 endpoint 行为一致。
2026-09-08 15:28:29 +08:00
saas-backend-agent 24b28bfc89 feat: #1795 文案库 CRUD(Script 模型 + Service + API + 迁移 + 35 测试)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 17s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 51s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m59s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m59s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m16s
AI Code Review / AI Code Review (pull_request) Successful in 6m23s
CI/CD Pipeline / Validate - Security (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 Merge on CI Green + Approved (pull_request) Successful in 5m28s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 192h3m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 192h3m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 192h3m44s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 192h3m47s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 192h3m47s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 192h3m48s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 192h3m48s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 192h3m49s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 192h3m50s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 192h3m51s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 192h38m21s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 192h38m31s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 192h38m32s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 192h38m34s
- 新建 ScriptModel (packages/adapters/sqlalchemy_impl/models.py)
  字段: id, user_id(indexed), title, content, segments(JSON), tags(JSON), created_at, updated_at
- 新建 script_service.py: CRUD 封装,用户隔离,tag 筛选,分页
- 新建 schemas/script.py: Pydantic request/response schemas
- 新建 routes/scripts.py: RESTful API (GET/POST/PUT/DELETE /api/v1/scripts)
- 新建 alembic 070_add_scripts_table.py: scripts 表 + user_id 索引
- 注册路由到 router.py (prefix=/scripts, tag=ScriptLibrary)
- 35 单元测试: service 15 + schema/route 20
2026-09-08 15:19:57 +08:00
xiaoxia 6904fce511 fix: #1789 标题字号滑块拖动回弹 — useMemo 稳定引用 + ref 防御 (#1793)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m20s
CI/CD Pipeline / Integration Tests (push) Successful in 1m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m42s
CI/CD Pipeline / Validate - Style (push) Successful in 2m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m27s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m30s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Validate - Security (push) Successful in 6m17s
CI/CD Pipeline / Unit Tests (push) Failing after 8m32s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 193h46m30s
CI/CD Pipeline / Build Production Web Image (push) Failing after 193h46m34s
CI/CD Pipeline / Deploy Production (push) Failing after 193h46m30s
CI/CD Pipeline / Build Production API Image (push) Failing after 193h46m34s
CI/CD Pipeline / CI Gate (push) Failing after 193h46m34s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 193h53m43s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 193h53m44s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 193h53m44s
CI/CD Pipeline / Frontend Lint (push) Failing after 193h55m8s
CI/CD Pipeline / PR Build Web Image (push) Failing after 193h55m13s
CI/CD Pipeline / PR Build API Image (push) Failing after 193h55m13s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 193h55m14s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 194h21m18s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 194h29m57s
2026-09-08 13:28:50 +08:00
xiaoxia 9dec75c365 feat: #1789 标题 drawtext 滤镜渲染 — 单视频+批量生成都生效 (#1792)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m12s
CI/CD Pipeline / Integration Tests (push) Successful in 1m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m45s
CI/CD Pipeline / Validate - Style (push) Successful in 2m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m14s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m29s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m41s
CI/CD Pipeline / Validate - Security (push) Successful in 6m20s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m30s
CI/CD Pipeline / Unit Tests (push) Failing after 7m32s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 194h2m10s
CI/CD Pipeline / CI Gate (push) Failing after 194h2m14s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 194h2m14s
CI/CD Pipeline / Build Production Web Image (push) Failing after 194h2m14s
CI/CD Pipeline / Build Production API Image (push) Failing after 194h2m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 194h8m11s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 194h8m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 194h9m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 194h9m52s
CI/CD Pipeline / PR Build API Image (push) Failing after 194h9m52s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 194h9m52s
CI/CD Pipeline / Deploy Production (push) Failing after 194h36m55s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 194h42m57s
CI/CD Pipeline / PR Build Web Image (push) Failing after 194h44m37s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 13:14:13 +08:00
xiaoxia 7997f083a5 fix: #1789 标题设置 UI 控件 + #1790 空模板时间线不显示刻度 (#1791)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 21s
CI/CD Pipeline / Build Staging API Image (push) Successful in 22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 52s
CI/CD Pipeline / Integration Tests (push) Successful in 1m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m6s
CI/CD Pipeline / Validate - Style (push) Successful in 2m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m31s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m31s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m32s
CI/CD Pipeline / Unit Tests (push) Failing after 7m51s
CI/CD Pipeline / Validate - Security (push) Successful in 9m14s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 195h20m58s
CI/CD Pipeline / CI Gate (push) Failing after 195h21m2s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 195h21m2s
CI/CD Pipeline / Build Production API Image (push) Failing after 195h21m2s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 195h29m15s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 195h29m15s
CI/CD Pipeline / PR Build Web Image (push) Failing after 195h30m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 195h29m16s
CI/CD Pipeline / PR Build API Image (push) Failing after 195h30m16s
CI/CD Pipeline / Frontend Lint (push) Failing after 195h30m16s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 195h30m19s
CI/CD Pipeline / Deploy Production (push) Failing after 195h55m42s
CI/CD Pipeline / Build Production Web Image (push) Failing after 195h55m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 196h4m59s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 11:53:47 +08:00
xiaoxia cf195685b9 Merge pull request 'feat: #1768 节奏曲线模板多样化 — 8种预设 + 时长钳制 + 误差校验' (#1788) from feat/rhythm-templates-diversify-1768 into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 26s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 55s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 45s
CI/CD Pipeline / Integration Tests (push) Successful in 2m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m35s
CI/CD Pipeline / Validate - Style (push) Successful in 2m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m18s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m36s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m22s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m57s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
AI Code Review / AI Code Review (pull_request) Successful in 7m19s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 13s
CI/CD Pipeline / Unit Tests (push) Failing after 8m30s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Security (push) Successful in 8m54s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 196h11m0s
CI/CD Pipeline / Build Production Web Image (push) Failing after 196h11m5s
CI/CD Pipeline / Deploy Production (push) Failing after 196h11m0s
CI/CD Pipeline / Build Production API Image (push) Failing after 196h11m5s
CI/CD Pipeline / CI Gate (push) Failing after 196h11m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 196h15m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 196h18m36s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 196h18m37s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 196h19m23s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 196h18m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 196h19m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 196h19m29s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 196h19m28s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 196h19m47s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 196h19m48s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 196h19m49s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 196h19m54s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 196h19m55s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 196h19m57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 196h19m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 196h19m57s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 196h19m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 196h19m59s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 196h19m57s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 196h19m59s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 196h19m59s
CI/CD Pipeline / Frontend Lint (push) Failing after 196h20m1s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 196h19m59s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 196h20m1s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 196h20m3s
CI/CD Pipeline / PR Build Web Image (push) Failing after 196h20m0s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 196h45m49s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 196h54m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 196h54m20s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 196h54m38s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 196h54m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 196h54m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 196h54m44s
2026-09-08 11:04:03 +08:00
xiaoxia ee4636e087 feat: #1768 节奏曲线模板多样化 — 8种预设 + 时长钳制 + 误差校验
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 55s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m32s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m48s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m56s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m51s
AI Code Review / AI Code Review (pull_request) Successful in 6m18s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 6m17s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 8s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 34s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 196h20m23s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 196h20m26s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 196h20m26s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 196h20m23s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 196h26m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 196h26m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 196h26m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 196h26m42s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 196h26m42s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 196h26m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 196h26m43s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 196h26m46s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 196h26m47s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 196h26m46s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 196h55m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 197h1m19s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 197h1m25s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 197h1m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 197h1m30s
变更:
- RHYTHM_TEMPLATES 从 6 种扩展到 8 种(新增 [2,2,1,1,2] 和 [3,2,1,2,1])
- 修复 MIN_CLIP_DURATION 被重复定义为 1.0 的 bug(恢复为 2.0)
- plan_clip_durations 新增 asset_durations 参数,钳制最大片段时长 <= 素材可用时长 × 90%
- 新增时长总和误差校验(成片净时长与配音时长误差 <= 0.5s),超限时末段补偿修正
- edit_plan_service.apply_voice_duration_to_plan 传入素材时长参与钳制
- 48 个新增单元测试全部通过

向后兼容:asset_durations 默认 None,不影响现有调用方
2026-09-08 10:57:16 +08:00
xiaoxia 5678779232 feat: #1776 素材库 asset_count 自动同步维护 (#1787)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 12s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m58s
CI/CD Pipeline / Integration Tests (push) Successful in 2m1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m2s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 2m32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 26s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m56s
CI/CD Pipeline / Build Staging API Image (push) Successful in 59s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m9s
CI/CD Pipeline / CI Gate (pull_request) Successful in 13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
AI Code Review / AI Code Review (pull_request) Successful in 6m29s
CI/CD Pipeline / Validate - Security (push) Successful in 6m24s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m40s
CI/CD Pipeline / Unit Tests (push) Failing after 10m13s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 196h43m21s
CI/CD Pipeline / Build Production Web Image (push) Failing after 196h43m25s
CI/CD Pipeline / Deploy Production (push) Failing after 196h43m21s
CI/CD Pipeline / Build Production API Image (push) Failing after 196h43m25s
CI/CD Pipeline / CI Gate (push) Failing after 196h43m25s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 196h49m9s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 196h49m10s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 196h49m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 196h49m11s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 196h51m9s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 196h51m14s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 196h51m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 196h51m15s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 196h51m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 196h51m22s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 196h51m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 196h51m50s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 196h51m24s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 196h51m54s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 196h51m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 196h53m10s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 196h53m10s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 196h53m10s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 196h53m11s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 196h53m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 196h53m13s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 196h53m40s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 196h53m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 196h53m41s
CI/CD Pipeline / PR Build Web Image (push) Failing after 196h53m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 196h53m41s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 196h53m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 196h53m46s
CI/CD Pipeline / PR Build API Image (push) Failing after 196h53m42s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 197h18m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 197h26m31s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 197h27m53s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 197h27m54s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 10:30:21 +08:00
xiaoxia 44224cfaf6 feat: 转场位置与类型随机化 (#1766) (#1786)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 7s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
CI/CD Pipeline / Build Staging API Image (push) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 23s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 29s
CI/CD Pipeline / CI Gate (pull_request) Successful in 18s
CI/CD Pipeline / Validate - Style (push) Successful in 1m43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m57s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m0s
CI/CD Pipeline / Integration Tests (push) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m5s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m7s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m56s
CI/CD Pipeline / Validate - Security (push) Successful in 5m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m24s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m33s
CI/CD Pipeline / Unit Tests (push) Failing after 9m24s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 196h56m39s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 196h56m42s
CI/CD Pipeline / Deploy Production (push) Failing after 196h56m39s
CI/CD Pipeline / Build Production Web Image (push) Failing after 196h56m42s
CI/CD Pipeline / Build Production API Image (push) Failing after 196h56m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 197h1m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 197h4m37s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 197h4m37s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 197h4m38s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 197h4m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 197h4m56s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 197h5m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 197h5m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 197h5m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 197h5m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 197h5m16s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 197h6m1s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 197h5m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 197h6m3s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 197h6m3s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 197h6m4s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 197h6m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 197h6m7s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 197h6m7s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 197h6m7s
CI/CD Pipeline / PR Build API Image (push) Failing after 197h6m7s
CI/CD Pipeline / Frontend Lint (push) Failing after 197h6m7s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 197h6m13s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 197h6m17s
CI/CD Pipeline / CI Gate (push) Failing after 197h31m25s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 197h39m53s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 197h39m58s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 197h40m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 197h40m47s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 197h40m51s
CI/CD Pipeline / PR Build Web Image (push) Failing after 197h40m51s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 10:17:35 +08:00
xiaoxia 98cf571ab3 feat: #1767 BGM 池差异化分配,打破变体间音频指纹一致性 (#1785)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 51s
CI/CD Pipeline / Integration Tests (push) Successful in 1m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
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
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 197h8m29s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 197h8m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 197h9m19s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 197h9m21s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 197h9m22s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 197h10m9s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 197h10m11s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 197h10m35s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 197h10m48s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 197h10m54s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 197h11m4s
CI/CD Pipeline / Frontend Lint (push) Failing after 197h11m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 197h11m11s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 197h11m12s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 197h11m11s
CI/CD Pipeline / PR Build Web Image (push) Failing after 197h11m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 197h11m11s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 197h11m13s
CI/CD Pipeline / PR Build API Image (push) Failing after 197h11m13s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 197h11m14s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 197h43m14s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 197h44m4s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 197h44m50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 197h45m17s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 197h45m43s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 10:12:52 +08:00
xiaoxia 12a9efc65d fix(test): 移除 InMemory 并发测试改为顺序幂等验证 (#1784)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 49s
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 Worker Image (push) Successful in 36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m55s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m7s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
CI/CD Pipeline / Validate - Style (push) Successful in 2m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Security (push) Successful in 4m46s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m31s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m23s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Unit Tests (push) Failing after 7m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 198h4m56s
CI/CD Pipeline / Deploy Production (push) Failing after 198h4m56s
CI/CD Pipeline / CI Gate (push) Failing after 198h5m0s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 198h5m0s
CI/CD Pipeline / Build Production API Image (push) Failing after 198h5m0s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 198h9m17s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 198h10m50s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 198h11m18s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 198h10m52s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 198h11m25s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 198h10m52s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 198h11m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 198h11m40s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 198h12m23s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 198h12m24s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 198h12m24s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 198h12m25s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 198h12m54s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 198h12m55s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 198h12m55s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 198h12m56s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 198h12m57s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 198h12m58s
CI/CD Pipeline / Frontend Lint (push) Failing after 198h12m59s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 198h12m59s
CI/CD Pipeline / PR Build API Image (push) Failing after 198h13m1s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 198h13m1s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 198h13m3s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 198h12m59s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 198h12m59s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 198h13m0s
CI/CD Pipeline / Build Production Web Image (push) Failing after 198h39m43s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 198h46m7s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 198h46m17s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 198h47m6s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 198h47m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 198h47m39s
CI/CD Pipeline / PR Build Web Image (push) Failing after 198h47m43s
Squash merge PR #1784: 修复 #1775 并发测试在 CI 失败(InMemory 无 DB 约束)
2026-09-08 09:11:05 +08:00
xiaoxia 9c0474ef9c feat: #1775 默认项目/素材库幂等化(DB 唯一约束兜底)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 41s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m53s
CI/CD Pipeline / Integration Tests (push) Successful in 2m56s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m43s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Style (push) Successful in 3m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m7s
CI/CD Pipeline / Validate - Security (push) Successful in 5m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m54s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m13s
AI Code Review / AI Code Review (pull_request) Successful in 6m38s
CI/CD Pipeline / Unit Tests (push) Failing after 7m58s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Failing after 198h32m29s
CI/CD Pipeline / CI Gate (push) Failing after 198h32m31s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 198h32m31s
CI/CD Pipeline / Build Production Web Image (push) Failing after 198h32m31s
CI/CD Pipeline / Build Production API Image (push) Failing after 198h32m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 198h36m16s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 198h37m22s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 198h37m23s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 198h39m3s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 198h37m23s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 198h39m11s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 198h39m14s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 198h39m20s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 198h39m40s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 198h39m27s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 198h39m33s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 198h39m42s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 198h39m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 198h39m39s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 198h40m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 198h40m28s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 198h40m29s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 198h40m29s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 198h40m29s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 198h40m30s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 198h40m30s
CI/CD Pipeline / Frontend Lint (push) Failing after 198h40m30s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 198h40m37s
CI/CD Pipeline / PR Build API Image (push) Failing after 198h40m37s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 198h40m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 198h40m39s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 199h14m21s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 199h14m26s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 199h15m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 199h15m11s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 199h15m11s
CI/CD Pipeline / PR Build Web Image (push) Failing after 199h15m19s
Squash merge PR #1783 into develop
2026-09-08 08:43:31 +08:00
xiaoxia 691c811cd4 chore: 删除无引用的旧7步流程遗留组件 GenerationStatus (#1782)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 20s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 31s
CI/CD Pipeline / CI Gate (pull_request) Successful in 20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m8s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
CI/CD Pipeline / Integration Tests (push) Successful in 2m17s
CI/CD Pipeline / Validate - Style (push) Successful in 2m54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m18s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Validate - Security (push) Successful in 5m57s
AI Code Review / AI Code Review (pull_request) Successful in 6m25s
CI/CD Pipeline / Unit Tests (push) Failing after 8m20s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 206h28m53s
CI/CD Pipeline / Deploy Production (push) Failing after 206h28m53s
CI/CD Pipeline / Build Production Web Image (push) Failing after 206h28m57s
CI/CD Pipeline / Build Production API Image (push) Failing after 206h28m57s
CI/CD Pipeline / CI Gate (push) Failing after 206h28m57s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 206h33m32s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 206h36m23s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 206h36m42s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 206h36m23s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 206h36m24s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 206h36m47s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 206h36m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 206h36m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 206h36m54s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 206h37m0s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 206h37m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 206h37m2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 206h37m1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 206h37m13s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 206h37m2s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 206h37m14s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 206h37m2s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 206h37m14s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 206h37m15s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 206h37m16s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 206h37m15s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 206h37m16s
CI/CD Pipeline / Frontend Lint (push) Failing after 206h37m17s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 206h37m21s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 206h37m21s
CI/CD Pipeline / PR Build API Image (push) Failing after 206h37m21s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 206h37m22s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 207h3m39s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 207h11m30s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 207h11m54s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 207h11m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 207h11m57s
CI/CD Pipeline / PR Build Web Image (push) Failing after 207h12m3s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 00:46:49 +08:00
xiaoxia 2114b7e7ae fix: Step5 播放器上移+去完成提示;Step6 封面页去视频全宽居中 (#1781)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 14s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 14s
CI/CD Pipeline / Check push changed paths (push) Successful in 22s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 48s
CI/CD Pipeline / Build Staging API Image (push) Successful in 24s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m19s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m28s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m40s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m32s
CI/CD Pipeline / Integration Tests (push) Successful in 2m43s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Validate - Style (push) Successful in 2m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m0s
CI/CD Pipeline / Validate - Security (push) Successful in 6m12s
AI Code Review / AI Code Review (pull_request) Successful in 6m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m4s
CI/CD Pipeline / Unit Tests (push) Failing after 8m36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 10m56s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 12m44s
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 (pull_request) Failing after 206h39m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 206h39m24s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 206h39m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 206h39m24s
CI/CD Pipeline / Canary Release to Production (push) Failing after 206h43m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 206h39m25s
CI/CD Pipeline / CI Gate (push) Failing after 206h43m19s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 206h39m25s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 206h39m25s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 206h43m19s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 206h39m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 206h43m19s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 206h39m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 206h39m26s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 206h49m12s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 206h49m12s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 206h50m11s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 206h50m48s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 206h50m50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 206h51m45s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 206h51m45s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 206h51m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 206h51m54s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 206h51m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 206h51m59s
CI/CD Pipeline / PR Build Web Image (push) Failing after 206h51m59s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 206h51m50s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 206h51m52s
CI/CD Pipeline / PR Build API Image (push) Failing after 206h52m0s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 206h52m15s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 207h14m6s
CI/CD Pipeline / Build Production Web Image (push) Failing after 207h18m1s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 207h23m54s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 207h25m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 207h26m26s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 00:31:06 +08:00
xiaoxia 7766ba1479 fix(#1774): 模板不存在明确返回404并收敛双表异常降级逻辑 (#1779)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped 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 / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped 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 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
CI/CD Pipeline / Build Production Web Image (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
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 206h53m51s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 206h53m52s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 206h53m55s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 206h53m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 206h53m55s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 206h53m55s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 206h53m55s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 206h53m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 206h53m55s
CI/CD Pipeline / Frontend Lint (push) Failing after 206h53m56s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 206h54m2s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 206h54m2s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 207h28m33s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 207h28m36s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 207h28m36s
CI/CD Pipeline / PR Build API Image (push) Failing after 207h28m37s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 00:30:08 +08:00
xiaoxia ef686dde8f fix(#1769): 模板列表过滤无片段配置的无效模板,避免前端选中后400 (#1772)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 17s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 47s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 58s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m9s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m10s
CI/CD Pipeline / Validate - Style (push) Successful in 2m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m26s
AI Code Review / AI Code Review (pull_request) Successful in 6m24s
CI/CD Pipeline / Validate - Security (push) Successful in 7m3s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m39s
CI/CD Pipeline / Unit Tests (push) Successful in 9m55s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 207h3m47s
CI/CD Pipeline / Deploy Production (push) Failing after 207h3m47s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 207h3m50s
CI/CD Pipeline / Build Production Web Image (push) Failing after 207h3m51s
CI/CD Pipeline / Build Production API Image (push) Failing after 207h3m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 207h9m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 207h12m1s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 207h12m2s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 207h12m4s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 207h12m22s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 207h12m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 207h12m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 207h12m32s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 207h13m21s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 207h13m21s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 207h13m26s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 207h13m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 207h13m38s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 207h13m40s
CI/CD Pipeline / Frontend Lint (push) Failing after 207h13m45s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 207h13m48s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 207h13m48s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 207h13m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 207h13m49s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 207h13m50s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 207h13m53s
CI/CD Pipeline / CI Gate (push) Failing after 207h38m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 207h47m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 207h47m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 207h48m2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 207h48m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 207h48m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 207h48m29s
CI/CD Pipeline / PR Build Web Image (push) Failing after 207h48m30s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 00:10:13 +08:00
xiaoxia 01026156ae fix(#1777): Step2素材库只显示视频库 + 失效模板运行时自动回退 (#1780)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 22s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 38s
CI/CD Pipeline / Build Staging API Image (push) Successful in 40s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m13s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m30s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
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
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 207h14m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 207h14m41s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 207h14m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 207h14m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 207h14m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 207h14m54s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 207h15m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 207h15m4s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 207h15m6s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 207h15m14s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 207h15m7s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 207h15m16s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 207h16m28s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 207h16m28s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 207h16m30s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 207h16m28s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 207h16m33s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 207h16m28s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 207h16m43s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 207h16m29s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 207h16m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 207h16m49s
CI/CD Pipeline / Frontend Lint (push) Failing after 207h16m51s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 207h16m52s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 207h16m54s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 207h49m24s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 207h49m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 207h49m52s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 207h51m10s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 207h51m18s
CI/CD Pipeline / PR Build Web Image (push) Failing after 207h51m28s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 00:07:18 +08:00
xiaoxia 42c0885813 Merge pull request 'fix: #1765 generate_pixel_perturbation 支持 int seed 可复现' (#1778) from fix/pixel-perturbation-seed into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m5s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m24s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m55s
AI Code Review / AI Code Review (pull_request) Successful in 6m45s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 22s
CI/CD Pipeline / Build Staging API Image (push) Successful in 25s
CI/CD Pipeline / Integration Tests (push) Successful in 1m38s
CI/CD Pipeline / Validate - Style (push) Successful in 2m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m25s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m28s
CI/CD Pipeline / Unit Tests (push) Successful in 8m6s
CI/CD Pipeline / Validate - Security (push) Successful in 16m13s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 207h42m51s
CI/CD Pipeline / Build Production Web Image (push) Failing after 207h42m56s
CI/CD Pipeline / Deploy Production (push) Failing after 207h42m51s
CI/CD Pipeline / Build Production API Image (push) Failing after 207h42m56s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 207h58m42s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 207h58m42s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 207h56m11s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 207h56m11s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 207h56m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 208h16m30s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 208h16m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 208h19m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 208h19m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 208h19m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 208h19m44s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 208h20m45s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 208h20m46s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 208h20m48s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 208h22m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 208h22m3s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 208h22m25s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 208h22m25s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 208h22m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 208h22m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 208h22m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 208h22m27s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 208h22m28s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 207h59m10s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 208h22m33s
CI/CD Pipeline / PR Build API Image (push) Failing after 207h59m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 207h59m11s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 208h17m37s
CI/CD Pipeline / PR Build Web Image (push) Failing after 208h33m50s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 208h33m54s
CI/CD Pipeline / CI Gate (push) Failing after 208h17m37s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 208h33m23s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 208h54m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 208h56m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 208h57m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 208h57m6s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 208h57m8s
2026-09-07 23:01:36 +08:00
saas-backend-agent cd6d8615e6 test: #1765 补充 int seed 分支测试,diff coverage 达标
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m40s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m30s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m34s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 9s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m37s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 20m16s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 208h16m18s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 208h16m18s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 208h16m24s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 208h16m25s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 208h36m16s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 208h36m16s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 208h36m23s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 208h36m34s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 208h36m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 208h36m35s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 208h36m40s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 208h36m41s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 208h36m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 208h36m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 208h36m43s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 208h36m50s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 208h51m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 209h11m9s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 209h11m20s
新增 5 个测试覆盖 int seed 入参分支(reproducible/与 Random 对象
等价/None 默认),满足 diff coverage ≥60% 要求。
2026-09-07 22:47:22 +08:00
saas-backend-agent fcc7863b31 fix: #1765 generate_pixel_perturbation 支持 int seed 参数
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m11s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m44s
AI Code Review / AI Code Review (pull_request) Successful in 6m42s
CI/CD Pipeline / Validate - Security (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 Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 208h47m56s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 208h47m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 208h48m3s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 208h48m7s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 208h48m8s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 208h48m8s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 208h48m8s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 208h48m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 208h48m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 208h48m10s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 208h48m17s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 209h22m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 209h22m48s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 209h22m48s
与 get_rhythm_template(seed) 接口保持一致:传入 int 时自动构造
random.Random(seed),保证基于 variantSeed 可复现;Random 对象和
None 调用方式不变。
2026-09-07 22:35:49 +08:00
xiaoxia 6af2f3c08d Merge pull request 'fix: 修复 compute_duplicate_rate 方法丢失导致 27 个单测失败' (#1773) from fix/dedup-method-restore into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 23s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 20s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 12s
CI/CD Pipeline / Build Staging API Image (push) Successful in 38s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 35s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m52s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m35s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (push) Successful in 3m18s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m29s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
AI Code Review / AI Code Review (pull_request) Successful in 7m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Unit Tests (push) Successful in 9m6s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m11s
CI/CD Pipeline / Validate - Security (push) Successful in 22m1s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 208h38m51s
CI/CD Pipeline / CI Gate (push) Failing after 208h38m55s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 208h38m55s
CI/CD Pipeline / Build Production API Image (push) Failing after 208h38m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 208h56m56s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 208h57m59s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 208h58m0s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 208h58m1s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 208h58m13s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 208h58m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 208h58m32s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 208h58m7s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 209h0m24s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 209h0m24s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 209h0m25s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 209h0m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 209h0m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 209h0m54s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 209h0m52s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 209h0m54s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 209h0m53s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 209h0m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 209h0m57s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 209h0m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 209h0m59s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 209h0m56s
CI/CD Pipeline / PR Build API Image (push) Failing after 209h1m0s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 209h1m0s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 209h1m2s
CI/CD Pipeline / Deploy Production (push) Failing after 209h13m31s
CI/CD Pipeline / Build Production Web Image (push) Failing after 209h13m35s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 209h32m40s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 209h33m9s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 209h35m4s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 209h35m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 209h35m33s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 209h35m36s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 209h35m39s
2026-09-07 22:23:11 +08:00
CI Bot 410c390cf0 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m58s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m24s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m30s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 19m0s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m28s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 209h1m13s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 209h1m16s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 209h1m16s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 209h1m16s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 209h19m59s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 209h20m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 209h20m7s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 209h20m12s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 209h20m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 209h20m16s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 209h20m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 209h20m19s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 209h20m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 209h20m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 209h20m22s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 209h35m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 209h54m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 209h54m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 209h54m54s
2026-09-07 14:03:49 +00:00
saas-backend-agent 9f85d40855 fix: 修复 compute_duplicate_rate 方法被错误嵌套到模块函数内部
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m46s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m55s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 209h22m35s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 209h22m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 209h22m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 209h22m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 209h22m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 209h22m47s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 209h22m48s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 209h22m48s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 209h22m52s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 209h22m52s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 209h22m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 209h22m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 209h57m16s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 209h57m29s
PR #1762 查重增强时,compute_duplicate_rate 方法被意外放到
compute_structure_similarity 模块函数体内(return 之后的死代码),
导致 VideoDeduplicator 类丢失该方法,27 个旧单测 AttributeError。

将方法移回 VideoDeduplicator 类,缩进不变。
验证:test_duplicate_rate/test_duplicate_rate_scope/
test_dedup_1702/test_bad_fingerprint_filter/test_dedup_enhanced
共 78 个测试全部通过。
2026-09-07 22:01:13 +08:00
xiaoxia 034eaac695 Merge pull request 'feat: 像素级扰动滤镜降低平台查重风险 (Issue #1765)' (#1771) from feat/pixel-perturbation-1765 into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 47s
CI/CD Pipeline / Build Staging API Image (push) Successful in 49s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / Integration Tests (push) Successful in 1m56s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m7s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m52s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m36s
CI/CD Pipeline / Unit Tests (push) Failing after 8m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m22s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 10s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 10s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Security (push) Successful in 22m17s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 210h5m40s
CI/CD Pipeline / CI Gate (push) Failing after 210h5m42s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 210h5m42s
CI/CD Pipeline / Build Production API Image (push) Failing after 210h5m42s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h12m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h12m35s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h12m38s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h12m35s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 210h12m39s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h12m40s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 210h12m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 210h24m24s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 210h12m41s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 210h26m2s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 210h26m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 210h27m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 210h27m56s
CI/CD Pipeline / Frontend Lint (push) Failing after 210h27m56s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 210h28m4s
CI/CD Pipeline / Deploy Production (push) Failing after 210h40m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 210h40m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 210h47m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h47m19s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h47m19s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 210h47m20s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h47m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h0m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 210h47m20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h47m20s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h2m35s
2026-09-07 20:56:11 +08:00
saas-backend-agent 27dccf6591 fix: #1769 智能剪辑 Step2 素材加载 400 错误
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / Integration Tests (push) Successful in 1m41s
CI/CD Pipeline / Validate - Style (push) Successful in 2m27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m37s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 18s
CI/CD Pipeline / Build Staging API Image (push) Successful in 37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m20s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m51s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m33s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m21s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m50s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m25s
CI/CD Pipeline / Unit Tests (push) Failing after 8m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m16s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m1s
CI/CD Pipeline / Validate - Security (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 / Retag skipped Staging Worker Image (push) Failing after 210h41m31s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 210h41m32s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h41m37s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h41m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 210h41m51s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 210h41m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h41m52s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h41m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 210h42m0s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h42m5s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h42m10s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 210h42m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 210h43m25s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h43m25s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h42m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 210h43m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 210h43m26s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 210h43m26s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 210h43m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h43m27s
CI/CD Pipeline / Frontend Lint (push) Failing after 210h45m3s
CI/CD Pipeline / PR Build Web Image (push) Failing after 210h45m35s
CI/CD Pipeline / PR Build API Image (push) Failing after 210h45m36s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h45m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 210h45m38s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h16m11s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 211h16m48s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h16m55s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 211h18m4s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 211h18m5s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h18m6s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h20m15s
- 根因:模板 f7578c059c2e4dfca2236eb86cb6808a 是无效测试模板
  - is_active=false(已失效)
  - template_clip_configs=0(无片段配置)
  - template_segments=0(无旧版片段配置)
- 修复:_get_template_segments 添加详细错误日志,便于排查
- 前端应过滤 is_active=false 的模板
- 用户应选择 clip_config_count > 0 的有效模板
2026-09-07 20:38:35 +08:00
CI Bot ac7ab679b7 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 11s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m57s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m48s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
AI Code Review / AI Code Review (pull_request) Successful in 6m40s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m25s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 19m24s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 13s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m33s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h35m22s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h35m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h35m26s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 210h35m26s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h52m4s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h52m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h52m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h54m28s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h54m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 210h54m30s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 210h54m57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 210h54m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h54m57s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h54m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h54m59s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h10m1s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h26m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h29m8s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h29m37s
2026-09-07 12:28:56 +00:00
saas-backend-agent bb98620a8a feat: 像素级扰动滤镜降低平台查重风险 (Issue #1765)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 19s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 20s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h55m5s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h55m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 210h55m33s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 210h55m36s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 210h55m43s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h56m22s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 210h59m0s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h29m47s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h30m15s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h31m2s
- generate_pixel_perturbation():随机选 2-3 种滤镜组合
  - noise: 轻微噪声 (0.01~0.02)
  - unsharp: 锐化/柔化 (-0.5~+0.5)
  - curves: 对比度微调 (0.95~1.05)
  - color_balance: RGB 通道偏移
- _apply_pixel_perturbation():渲染时追加到 FFmpeg filter chain
- edit_plan_service:批量变体生成时为每个变体生成不同像素扰动
- 参数幅度确保肉眼不可见(SSIM > 0.95),帧级差异 > 3%
- 11 个新测试覆盖滤镜组合/参数范围/验收
2026-09-07 20:24:13 +08:00
xiaoxia 2a0b75c007 Merge pull request 'feat: 节奏模板让批量变体片段时长分布不同 (Issue #1764)' (#1770) from feat/rhythm-templates-1764 into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 6s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Check push changed paths (push) Successful in 35s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / Integration Tests (push) Successful in 1m42s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 39s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Style (push) Successful in 2m41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 42s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m41s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
CI/CD Pipeline / Unit Tests (push) Failing after 11m13s
AI Code Review / AI Code Review (pull_request) Failing after 10m34s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Failing after 12m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m22s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m3s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m32s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m25s
CI/CD Pipeline / Validate - Security (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 - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 210h49m25s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 210h55m9s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 210h55m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 210h55m28s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 210h55m11s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 210h55m27s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 210h55m30s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 210h55m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 210h58m29s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 210h59m6s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h0m7s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 210h59m8s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h0m8s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 210h59m8s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h1m29s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h2m11s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h2m5s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h2m20s
CI/CD Pipeline / PR Build Web Image (push) Failing after 211h2m6s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h2m8s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h34m46s
2026-09-07 20:21:38 +08:00
CI Bot 0d9d4e584f style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 27s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m34s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m10s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m51s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 49s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 12m53s
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 13m1s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 20m23s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 210h41m47s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 210h41m49s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 210h42m2s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 210h42m2s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 210h42m4s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h2m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h3m9s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h3m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h2m58s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 211h3m12s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h2m58s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h3m13s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h3m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h3m13s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h3m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h37m44s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h37m51s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h37m59s
2026-09-07 12:20:53 +00:00
saas-backend-agent eb089fa26d feat: 节奏模板让批量变体片段时长分布不同 (Issue #1764)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 36s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 37s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m31s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h5m53s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h5m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h6m2s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h6m2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h6m3s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h6m4s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h6m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h6m6s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h6m6s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h6m9s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h40m35s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h40m42s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 211h40m43s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h40m46s
- 新增 RHYTHM_TEMPLATES 节奏模板池(6 种权重序列)
- get_rhythm_template(seed):根据变体 seed 随机选模板
- adapt_template_length():将模板适配到实际片段数
- plan_clip_durations() 支持 rhythm_template 参数按权重分配时长
- 保底约束:每段 >= 2s,总时长 = 配音时长
- edit_plan_service: ensure_batch_variant_plans 为每个变体生成不同节奏模板
- 17 个新测试覆盖模板池/适配/分配/验收
2026-09-07 20:17:52 +08:00
saas-backend-agent 1ec274b9f5 ci: trigger push pipeline for dedup enhanced
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 12s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 7s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 50s
CI/CD Pipeline / Integration Tests (push) Successful in 1m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 55s
CI/CD Pipeline / Validate - Style (push) Successful in 2m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 30s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m49s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m0s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m59s
CI/CD Pipeline / Unit Tests (push) Failing after 7m56s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 6m11s
CI/CD Pipeline / Validate - Security (push) Successful in 16m42s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 211h15m1s
CI/CD Pipeline / CI Gate (push) Failing after 211h15m3s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 211h15m3s
CI/CD Pipeline / Build Production Web Image (push) Failing after 211h15m3s
CI/CD Pipeline / Build Production API Image (push) Failing after 211h15m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 211h27m48s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h29m4s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 211h28m58s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h29m4s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 211h29m5s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h29m4s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h29m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 211h29m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h29m5s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 211h29m8s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 211h29m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h29m8s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h29m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h31m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h29m11s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 211h31m37s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 211h31m39s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 211h31m37s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 211h31m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h31m43s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h31m43s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h31m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h31m50s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h31m49s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h31m51s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h31m56s
CI/CD Pipeline / Deploy Production (push) Failing after 211h49m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h3m43s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h3m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h6m16s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 212h6m17s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h6m21s
CI/CD Pipeline / PR Build Web Image (push) Failing after 212h6m28s
2026-09-07 19:51:52 +08:00
xiaoxia 62820391c3 fix: Step5 单视频全宽布局+视频播放器居中 (#1761)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Blocked by required conditions
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Blocked by required conditions
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Web Image (push) Blocked by required conditions
CI/CD Pipeline / PR Build Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Check push changed paths (push) Waiting to run
CI/CD Pipeline / Build Staging API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Staging Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging API Image (push) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging Web Image (push) Blocked by required conditions
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI/CD Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI/CD Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Build Production API Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Web Image (push) Blocked by required conditions
CI/CD Pipeline / Build Production Worker Image (push) Blocked by required conditions
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Production Browser E2E (push) Blocked by required conditions
CI/CD Pipeline / ACR Image Cleanup (push) Blocked by required conditions
CI/CD Pipeline / Canary Release to Production (push) Blocked by required conditions
CI/CD Pipeline / CI Gate (push) Blocked by required conditions
2026-09-07 19:51:42 +08:00
xiaoxia 322b0a082c Merge pull request 'feat(dedup): 增加文案+结构维度查重 (P2-后端3)' (#1762) from feat/dedup-enhanced-text-structure into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 16s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped 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 / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + 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 / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped 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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 58s
AI Code Review / AI Code Review (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
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h33m11s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h33m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 211h33m12s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h34m8s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h33m12s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h33m15s
2026-09-07 19:50:00 +08:00
CI Bot 07aa03da24 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 43s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m12s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m3s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m8s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m17s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m23s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 16m23s
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 211h18m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 211h18m32s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 211h18m32s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 211h18m32s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h34m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h34m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h34m28s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h34m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h34m30s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 211h34m47s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h34m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h34m57s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h34m59s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h34m57s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h53m10s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h9m5s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h9m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h9m30s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h9m36s
2026-09-07 11:49:16 +00:00
saas-backend-agent add93bb9de feat(dedup): 增加文案+结构维度查重 (Issue #P2-后端3)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m16s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m1s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 211h36m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h36m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 211h36m42s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h36m47s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 211h36m47s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h37m12s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h37m13s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 211h37m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h37m19s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h37m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h11m17s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h11m26s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h11m45s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h11m58s
- 新增 compute_text_similarity: 字符级 Jaccard 相似度比对配音文本
- 新增 compute_structure_similarity: 片段序列相似度(数量+类型+时长分布)
- 多维度融合公式: duplicate_rate = visual*0.5 + text*0.25 + structure*0.25
- 文案/结构数据缺失时降级为纯视觉维度
- 21 个新测试覆盖文本+结构+权重常量
2026-09-07 19:46:43 +08:00
xiaoxia 6fe3e03d6a Merge pull request 'feat: 封面智能选帧 — 帧质量评分(清晰度/亮度/色彩)' (#1759) from feat/cover-frame-scoring into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 39s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m51s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m43s
CI/CD Pipeline / Validate - Style (push) Successful in 2m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m39s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m32s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m30s
AI Code Review / AI Code Review (pull_request) Successful in 6m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m15s
CI/CD Pipeline / Unit Tests (push) Successful in 8m37s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m0s
CI/CD Pipeline / Validate - Security (push) Successful in 10m32s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 211h45m14s
CI/CD Pipeline / CI Gate (push) Failing after 211h45m18s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 211h45m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 211h51m51s
CI/CD Pipeline / Build Production Web Image (push) Failing after 211h45m18s
CI/CD Pipeline / Build Production API Image (push) Failing after 211h45m18s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 211h52m42s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 211h54m6s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 211h52m42s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 211h54m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 211h52m42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 211h54m15s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 211h55m31s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 211h54m22s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 211h55m31s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 211h55m33s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 211h55m33s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 211h55m41s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 211h55m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 211h55m42s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 211h55m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 211h55m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 211h55m44s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 211h55m45s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 211h55m46s
CI/CD Pipeline / PR Build API Image (push) Failing after 211h55m49s
CI/CD Pipeline / Frontend Lint (push) Failing after 211h55m51s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 211h55m53s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 211h55m54s
CI/CD Pipeline / Deploy Production (push) Failing after 212h19m53s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h28m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h29m1s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h30m12s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h30m13s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 212h30m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h30m21s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 212h30m23s
CI/CD Pipeline / PR Build Web Image (push) Failing after 212h30m27s
2026-09-07 19:28:22 +08:00
xiaoxia d73c0a77d8 fix: Step5 BatchGenerationGrid 单卡居中 + maxWidth 240px (#1760)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 22s
CI/CD Pipeline / Integration Tests (push) Successful in 1m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m31s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m48s
CI/CD Pipeline / Validate - Style (push) Successful in 2m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m28s
CI/CD Pipeline / Validate - Security (push) Successful in 5m33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
AI Code Review / AI Code Review (pull_request) Successful in 6m32s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m38s
CI/CD Pipeline / Unit Tests (push) Successful in 8m28s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 58s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m11s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h8m11s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 212h8m17s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h8m17s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h15m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h15m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h15m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h15m46s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h15m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h15m58s
CI/CD Pipeline / Canary Release to Production (push) Failing after 212h24m8s
CI/CD Pipeline / CI Gate (push) Failing after 212h24m12s
CI/CD Pipeline / Build Production Web Image (push) Failing after 212h24m12s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 212h24m12s
CI/CD Pipeline / Build Production API Image (push) Failing after 212h24m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 212h29m27s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 212h31m10s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 212h31m11s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 212h31m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h15m55s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h15m55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 212h15m57s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 212h15m55s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 212h15m57s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 212h15m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 212h32m38s
CI/CD Pipeline / PR Build Web Image (push) Failing after 212h32m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 212h32m43s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 212h32m46s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 212h42m50s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h42m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 212h50m15s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h50m25s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h50m31s
CI/CD Pipeline / Deploy Production (push) Failing after 212h58m47s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h50m25s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 212h50m35s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h50m31s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 213h7m22s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 18:51:29 +08:00
saas-backend-agent 1087ec181b fix: test_empty_frame_returns_zero 添加 cv2 可用性检测
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m15s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m24s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m11s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m17s
AI Code Review / AI Code Review (pull_request) Successful in 6m20s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 9m11s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m59s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 9s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 45s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h30m21s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 212h30m26s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h30m27s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h30m27s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h39m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h39m31s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h39m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h39m30s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h39m37s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h39m37s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h39m43s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h39m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h39m38s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h39m42s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h39m42s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 213h5m0s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h14m8s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h14m14s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h14m20s
2026-09-07 18:44:32 +08:00
CI Bot 04463fbdce style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m48s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m10s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m19s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m39s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 4s
AI Code Review / AI Code Review (pull_request) Successful in 6m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m9s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h43m42s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 212h43m45s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h43m45s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h50m12s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h50m12s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h43m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h50m17s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h50m22s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h50m22s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h50m22s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h50m23s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h50m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h50m24s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h50m25s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h50m30s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 213h18m20s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h24m50s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 213h25m0s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h25m1s
2026-09-07 10:33:42 +00:00
saas-backend-agent fadca408d4 fix: 测试添加 cv2 可用性检测,mock 环境下 skip
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
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
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h52m31s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 212h52m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h52m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h52m45s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h52m38s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h52m45s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h52m38s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h52m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h52m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h52m43s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h52m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h52m50s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 213h27m10s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h27m24s
2026-09-07 18:31:18 +08:00
saas-backend-agent c6a3137949 fix: 加强 cv2 mock 检测(用 inspect.isroutine 验证)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + 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 / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h53m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 212h53m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h53m13s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h53m14s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 212h53m14s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h53m16s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 212h53m21s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h53m4s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h53m27s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h53m22s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h27m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h27m53s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 213h27m55s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h28m1s
2026-09-07 18:30:39 +08:00
saas-backend-agent 3447ab2582 fix: 修复 CI 测试失败
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + 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 / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 212h54m5s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 212h54m7s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 212h54m8s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 212h54m17s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 212h54m18s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 212h54m18s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 212h54m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 212h54m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 212h54m23s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 212h54m28s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 212h54m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 213h28m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h29m1s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 213h29m2s
1. cover_frame_scorer.py: 加强 cv2/numpy 真实性检查,防止 mock 对象
2. test_generation_cover.py: 更新 max_frames 断言 1→5
2026-09-07 18:29:43 +08:00
CI Bot 37ebf42278 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m30s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m43s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 4m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 6m18s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m33s
AI Code Review / AI Code Review (pull_request) Successful in 6m59s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 212h56m31s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 212h56m34s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 212h56m31s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 212h56m34s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 213h2m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h2m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 213h2m42s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h2m49s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 213h2m50s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 213h2m52s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 213h2m53s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 213h2m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h2m56s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 213h3m0s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 213h31m13s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 213h37m12s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 213h37m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 213h37m31s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h37m34s
2026-09-07 10:21:04 +00:00
saas-backend-agent d6a2715a26 feat: 封面智能选帧 — 帧质量评分(清晰度/亮度/色彩)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m5s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m17s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 2m29s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m29s
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
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 213h5m24s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 213h5m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 213h5m29s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 213h5m32s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 213h5m33s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 213h5m32s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 213h5m33s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 213h5m39s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 213h5m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 213h5m39s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 213h5m40s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 213h40m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 213h40m11s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 213h40m13s
P1-后端2:封面智能选帧

新增 cover_frame_scorer.py:
- score_frame(): 评估单帧质量 0~100 分
  - 清晰度 (0~40): 拉普拉斯方差,越高越清晰
  - 亮度 (0~30): 80~180 舒适区间满分
  - 色彩丰富度 (0~30): RGB 三维直方图非零 bin
- score_frames(): 批量评分+排序
- select_best_frame(): 选出最佳帧
- 降级策略:cv2 不可用时返回 50.0 默认分

修改 thumbnail_generator.py:
- extract_and_upload_cover_frames: num_frames=3→5
- 抽帧后对所有候选帧进行质量评分
- 按分数从高到低排序返回
- 统一临时文件管理(评分后清理)

修改 generation_cover.py:
- E1/E2 步骤 max_frames=1→5
- 新增 _select_best_frame_from_snapshots() 辅助函数
- MediaKit 抽多帧后通过质量评分选最佳

修改 render_adapter.py:
- 调用 extract_and_upload_cover_frames 时 num_frames=5

测试:test_cover_frame_scorer.py(18 个测试)
2026-09-07 18:18:19 +08:00
xiaoxia b8073b6c38 fix: Step5卡片黑边+缩放 & 配音时长对齐纳入转场时长 (#1756) (#1758)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m5s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m25s
AI Code Review / AI Code Review (pull_request) Successful in 6m17s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / Integration Tests (push) Successful in 1m41s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m4s
CI/CD Pipeline / Validate - Style (push) Successful in 2m16s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m22s
CI/CD Pipeline / Validate - Security (push) Successful in 6m49s
CI/CD Pipeline / Unit Tests (push) Successful in 7m57s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 214h16m41s
CI/CD Pipeline / CI Gate (push) Failing after 214h16m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 214h16m45s
CI/CD Pipeline / Build Production API Image (push) Failing after 214h16m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 214h24m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 214h24m13s
CI/CD Pipeline / Frontend Lint (push) Failing after 214h24m44s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 214h23m3s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 214h23m3s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 214h24m41s
CI/CD Pipeline / Build Staging API Image (push) Failing after 214h24m42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 214h40m36s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 214h43m55s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 214h43m58s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 214h44m0s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 214h44m0s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 214h44m1s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 214h44m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 214h44m2s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 214h44m1s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 214h44m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 214h44m2s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 214h44m3s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 214h44m4s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 214h44m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 214h44m4s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 214h24m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 214h24m43s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 214h44m9s
CI/CD Pipeline / Deploy Production (push) Failing after 214h51m20s
CI/CD Pipeline / Build Production Web Image (push) Failing after 214h51m24s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 214h58m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 214h59m21s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 214h59m26s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 214h57m42s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 214h59m20s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 215h18m34s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 215h18m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 215h18m41s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 215h18m41s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 215h18m42s
2026-09-07 16:39:24 +08:00
xiaoxia debb3a5d67 feat(#1758): FFmpeg 渲染加速——preset medium→fast + 多线程 + 环境变量可覆盖 (#1757)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 56s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 32s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + 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 Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped 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 / PR Build Web Image (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
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 214h45m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 214h45m44s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 214h45m38s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 214h45m45s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 214h45m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 214h45m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 214h45m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 214h46m29s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 214h46m30s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 214h46m31s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 214h46m31s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 214h46m31s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 214h46m32s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 214h46m36s
CI/CD Pipeline / Frontend Lint (push) Failing after 214h46m38s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 214h46m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 214h46m40s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 214h46m40s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 214h46m40s
CI/CD Pipeline / PR Build API Image (push) Failing after 214h46m41s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 214h46m41s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 214h46m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 215h20m17s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 215h20m25s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 215h21m8s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 215h21m9s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 215h21m10s
CI/CD Pipeline / PR Build Web Image (push) Failing after 215h21m18s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 16:37:35 +08:00
xiaoxia 87a61b7f2b fix(#1754): ASS 标题位置常量同步前端改动(TOP 120→180, BOTTOM 60→100) (#1756)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Build Staging API Image (push) Successful in 19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m11s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
CI/CD Pipeline / Validate - Style (push) Successful in 2m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m31s
CI/CD Pipeline / Validate - Security (push) Successful in 5m55s
AI Code Review / AI Code Review (pull_request) Successful in 6m21s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m49s
CI/CD Pipeline / Unit Tests (push) Successful in 7m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 216h19m8s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 216h19m11s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 216h26m0s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 216h26m0s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 216h26m6s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 216h26m9s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 216h26m9s
CI/CD Pipeline / Canary Release to Production (push) Failing after 216h36m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 216h26m10s
CI/CD Pipeline / CI Gate (push) Failing after 216h36m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 216h36m45s
CI/CD Pipeline / Build Production API Image (push) Failing after 216h36m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 216h41m2s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 216h42m13s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 216h42m13s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 216h42m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 216h26m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 216h26m8s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 216h26m8s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 216h26m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 216h26m9s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 216h44m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 216h44m44s
CI/CD Pipeline / Frontend Lint (push) Failing after 216h44m45s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 216h44m49s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 216h53m45s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 216h53m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h0m41s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 217h0m44s
CI/CD Pipeline / Deploy Production (push) Failing after 217h11m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 217h11m22s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h0m37s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h0m43s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 216h53m48s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h0m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 217h0m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 217h0m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h19m21s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 14:39:27 +08:00
xiaoxia 4b3feb2d88 feat(#1754): 渲染期画面扰动——让批量视频本身更不同 (#1755)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 8s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 12s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m35s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m6s
AI Code Review / AI Code Review (pull_request) Successful in 6m35s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 15s
CI/CD Pipeline / Build Staging API Image (push) Successful in 18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 49s
CI/CD Pipeline / Integration Tests (push) Successful in 1m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m34s
CI/CD Pipeline / Validate - Style (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m55s
CI/CD Pipeline / Validate - Security (push) Successful in 6m9s
CI/CD Pipeline / Unit Tests (push) Successful in 6m53s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 217h5m50s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 217h5m52s
CI/CD Pipeline / Deploy Production (push) Failing after 217h5m50s
CI/CD Pipeline / Build Production API Image (push) Failing after 217h5m52s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 217h12m23s
CI/CD Pipeline / PR Build API Image (push) Failing after 217h12m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 217h12m45s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 217h11m27s
CI/CD Pipeline / Staging API Integration Tests (push) Failing after 217h11m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 217h25m2s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 217h27m26s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 217h27m28s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h27m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 217h27m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 217h28m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h27m30s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 217h28m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h28m7s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 217h28m8s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 217h28m8s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 217h28m9s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 217h28m17s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 217h28m10s
CI/CD Pipeline / CI Gate (push) Failing after 217h40m30s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 217h47m1s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 217h47m23s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 217h47m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 217h46m5s
CI/CD Pipeline / Build Production Web Image (push) Failing after 217h40m30s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 217h47m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 218h2m6s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 218h2m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 218h2m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 218h2m47s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 218h2m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h47m23s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 13:55:47 +08:00
xiaoxia d8da69970b fix(generate): #1754 预览卡片黑边+占比过大/配音时长兜底调速/标题位置优化 (#1754)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 14s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 16s
CI/CD Pipeline / Build Staging API Image (push) Successful in 26s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 58s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 37s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m6s
CI/CD Pipeline / Validate - Style (push) Successful in 2m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
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
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 217h29m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 217h29m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 217h29m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 217h30m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 217h30m1s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 217h30m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 217h30m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 217h30m1s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 217h30m6s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 217h30m7s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 217h31m44s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 217h30m7s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 217h31m44s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 217h31m48s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 217h31m45s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 217h31m49s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 217h31m45s
CI/CD Pipeline / Frontend Lint (push) Failing after 217h31m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 217h31m46s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 217h31m54s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 217h31m46s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 217h31m47s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 217h31m54s
CI/CD Pipeline / PR Build Web Image (push) Failing after 217h31m54s
CI/CD Pipeline / PR Build API Image (push) Failing after 217h31m54s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 217h31m56s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 218h4m33s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 218h4m37s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 218h4m44s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 218h4m49s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 218h6m25s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 13:52:23 +08:00
xiaoxia 3557635f93 fix(#1749): variant-plans clips 补 status 字段(P0 批量预览过滤修复) (#1753)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 12s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 29s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 19s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m27s
CI/CD Pipeline / Integration Tests (push) Successful in 1m57s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m1s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m11s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Validate - Security (push) Successful in 6m25s
AI Code Review / AI Code Review (pull_request) Successful in 6m32s
CI/CD Pipeline / Unit Tests (push) Successful in 7m34s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 20m25s
CI/CD Pipeline / Canary Release to Production (push) Failing after 220h7m48s
CI/CD Pipeline / Deploy Production (push) Failing after 220h7m48s
CI/CD Pipeline / Build Production API Image (push) Failing after 220h7m52s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 220h12m8s
CI/CD Pipeline / CI Gate (push) Failing after 220h7m52s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 220h13m23s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 220h7m52s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 220h13m26s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 220h14m46s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 220h14m55s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 220h14m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 220h14m55s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 220h14m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 220h15m12s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 220h15m15s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 220h15m17s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 220h15m20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 220h15m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 220h15m24s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 220h15m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 220h15m26s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 220h15m25s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 220h15m26s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 220h15m27s
CI/CD Pipeline / Frontend Lint (push) Failing after 220h15m28s
CI/CD Pipeline / PR Build API Image (push) Failing after 220h15m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 220h15m31s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 220h15m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 220h15m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 220h42m29s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 220h48m1s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 220h49m26s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 220h49m49s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 220h49m52s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 220h50m1s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 220h50m2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 220h50m3s
CI/CD Pipeline / PR Build Web Image (push) Failing after 220h50m9s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 11:08:46 +08:00
xiaoxia e83a0b86f8 fix(#1749): 批量三连修:配音严格守卫/配音时长分配+末帧冻结/素材级去重 + variant-plans 接口 (#1752)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 58s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 55s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
CI/CD Pipeline / Validate - Style (push) Successful in 2m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m30s
CI/CD Pipeline / Integration Tests (push) Successful in 2m35s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m15s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Security (push) Successful in 4m34s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m37s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m54s
AI Code Review / AI Code Review (pull_request) Successful in 6m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Unit Tests (push) Successful in 8m33s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m48s
CI/CD Pipeline / Canary Release to Production (push) Failing after 221h53m0s
CI/CD Pipeline / CI Gate (push) Failing after 221h53m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 221h53m4s
CI/CD Pipeline / Build Production API Image (push) Failing after 221h53m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 221h58m26s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 221h59m0s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 221h59m0s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 222h0m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 222h0m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 221h59m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 222h0m57s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 222h0m55s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 222h1m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 222h1m17s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 222h0m55s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 222h1m23s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 222h1m38s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 222h1m28s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 222h1m39s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 222h1m33s
CI/CD Pipeline / Frontend Lint (push) Failing after 222h1m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 222h1m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 222h1m43s
CI/CD Pipeline / PR Build API Image (push) Failing after 222h1m43s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 222h1m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 222h1m42s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 222h1m45s
CI/CD Pipeline / Deploy Production (push) Failing after 222h27m37s
CI/CD Pipeline / Build Production Web Image (push) Failing after 222h27m42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 222h35m33s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 222h35m50s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 222h36m14s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 222h36m15s
CI/CD Pipeline / PR Build Web Image (push) Failing after 222h36m20s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 09:22:34 +08:00
xiaoxia 478c9c2bc3 fix(generate): #1750 批量预览五连修——删除假数据 fallback/配音时长/竖屏播放器/封面一键生成 (#1751)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / Build Staging API Image (push) Successful in 16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 27s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m1s
CI/CD Pipeline / Validate - Style (push) Successful in 2m14s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m7s
CI/CD Pipeline / Integration Tests (push) Successful in 2m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
CI/CD Pipeline / Validate - Security (push) Successful in 5m13s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m54s
AI Code Review / AI Code Review (pull_request) Successful in 6m20s
CI/CD Pipeline / Unit Tests (push) Failing after 7m50s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m6s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 7m15s
CI/CD Pipeline / Canary Release to Production (push) Failing after 229h47m32s
CI/CD Pipeline / CI Gate (push) Failing after 229h47m46s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 229h47m46s
CI/CD Pipeline / Build Production API Image (push) Failing after 229h47m46s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 229h52m6s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 229h52m58s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 229h52m58s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 229h54m40s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 229h54m49s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 229h54m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 229h54m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 229h54m55s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 229h54m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 229h54m56s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 229h55m12s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 229h55m12s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 229h55m34s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 229h55m33s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 229h55m34s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 229h55m34s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 229h55m35s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 229h55m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 229h55m39s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 229h55m39s
CI/CD Pipeline / Frontend Lint (push) Failing after 229h55m40s
CI/CD Pipeline / PR Build Web Image (push) Failing after 229h55m40s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 229h55m43s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 229h55m44s
CI/CD Pipeline / Deploy Production (push) Failing after 230h22m21s
CI/CD Pipeline / Build Production Web Image (push) Failing after 230h22m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 230h27m35s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 230h29m26s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 230h29m32s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 230h29m49s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 230h30m11s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 230h30m11s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 230h30m16s
CI/CD Pipeline / PR Build API Image (push) Failing after 230h30m17s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-07 01:28:34 +08:00
xiaoxia bcbcb41750 feat(generate): #1744 批量预览对接后端变体真实片段 + 智能匹配候选池×3 (#1748)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 11s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m27s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Validate - Style (push) Successful in 2m26s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m37s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m13s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m26s
CI/CD Pipeline / Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m34s
AI Code Review / AI Code Review (pull_request) Successful in 6m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Failing after 8m1s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m11s
CI/CD Pipeline / Validate - Security (push) Successful in 18m6s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 234h20m56s
CI/CD Pipeline / CI Gate (push) Failing after 234h20m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 234h20m59s
CI/CD Pipeline / Build Production Web Image (push) Failing after 234h20m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 234h35m39s
CI/CD Pipeline / Build Production API Image (push) Failing after 234h21m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 234h36m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 234h36m6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 234h37m27s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 234h37m31s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 234h37m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 234h37m37s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 234h38m28s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 234h39m0s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 234h38m31s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 234h38m39s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 234h39m0s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 234h38m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 234h39m1s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 234h38m49s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 234h39m1s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 234h38m55s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 234h39m1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 234h39m2s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 234h39m2s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 234h39m2s
CI/CD Pipeline / PR Build Web Image (push) Failing after 234h39m2s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 234h39m8s
CI/CD Pipeline / PR Build API Image (push) Failing after 234h39m3s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 234h39m10s
CI/CD Pipeline / Frontend Lint (push) Failing after 234h39m5s
CI/CD Pipeline / Deploy Production (push) Failing after 234h55m33s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 235h10m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h12m8s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 235h12m11s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h13m35s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 235h13m36s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 235h13m37s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 20:45:10 +08:00
xiaoxia 4c03e05e6c fix(#1743): smart-match排序随机噪声 + 素材使用次数按成片实际片段计数 (#1747)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging API Image (push) Successful in 45s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m24s
CI/CD Pipeline / Integration Tests (push) Successful in 2m33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m34s
AI Code Review / AI Code Review (pull_request) Failing after 3m19s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m39s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m44s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m39s
CI/CD Pipeline / Unit Tests (push) Failing after 9m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m27s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 14s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 50s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Security (push) Successful in 19m9s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 234h47m57s
CI/CD Pipeline / CI Gate (push) Failing after 234h47m59s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 234h47m59s
CI/CD Pipeline / Build Production API Image (push) Failing after 234h47m59s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 234h52m2s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 234h52m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 234h52m9s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 234h52m10s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 234h52m10s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 234h52m10s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 234h52m11s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 234h52m15s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 234h52m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 234h52m15s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 234h52m16s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 235h3m39s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 235h4m37s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 235h4m37s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 235h4m38s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 235h7m9s
CI/CD Pipeline / PR Build API Image (push) Failing after 235h7m9s
CI/CD Pipeline / Frontend Lint (push) Failing after 235h7m9s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 235h7m14s
CI/CD Pipeline / Deploy Production (push) Failing after 235h22m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 235h22m35s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h26m41s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 235h26m44s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 235h26m46s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h26m52s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 235h26m57s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 235h26m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 235h26m48s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 235h26m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 235h26m53s
CI/CD Pipeline / PR Build Web Image (push) Failing after 235h41m46s
2026-09-06 20:17:07 +08:00
saas-backend-agent 6cdb70bb61 test(#1743): 修复 smart-match 随机噪声引发的确定性排序断言
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 14s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 15s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m22s
AI Code Review / AI Code Review (pull_request) Successful in 1m31s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m35s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m49s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m7s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m31s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 14m41s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 7s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m30s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 235h10m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 235h10m29s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 235h10m36s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 235h25m7s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 235h10m36s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 235h25m10s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 235h10m36s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 235h25m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 235h25m13s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 235h25m14s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h25m14s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 235h25m22s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 235h25m15s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 235h25m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 235h25m20s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h59m44s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 235h59m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 235h59m51s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 235h59m57s
CI Unit Tests 暴露(沙箱 httpx2 无法 collect test_smart_match*.py 未本地复现):
- test_smart_match_fallback test_limit_backfills: second(quality=40)/third(30)
  质量项仅差 4 分 < 噪声 20,third 可翻到首位 → second 质量提至 90(差 24>20),
  200 seeds 验证零翻转
- test_smart_match test_returns_sorted_by_score_descending / test_image_assets:
  smart_select_assets 直接调用处注入零噪声 rng(_ZeroNoiseRandom),验证纯评分排序
- 验证生产噪声行为:大分差素材排名稳定、同分素材组合随机
2026-09-06 19:58:59 +08:00
saas-backend-agent ed09794f4d fix(#1743): smart-match排序注入随机噪声 + 素材使用次数按成片实际片段计数回写
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 34s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m27s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 1m25s
AI Code Review / AI Code Review (pull_request) Successful in 1m32s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m48s
CI/CD Pipeline / Validate - Security (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 Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 235h35m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 235h35m43s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 235h35m44s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 235h35m40s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 235h35m44s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 235h35m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 235h35m45s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 235h35m45s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 235h35m46s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 235h35m46s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 235h35m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 236h10m20s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 236h10m21s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 236h10m22s
1. smart-match 排序零随机修复(主因):
   - smart_select_assets 排序/多样性分桶注入 0~SCORE_RANDOM_NOISE_MAX 随机噪声,
     同分/近分素材每次选出不同组合与顺序;分差>20的高质量素材保持稳定优先级
   - 噪声以 asset.id 为 key 同次调用内一致;r.score 始终为无噪声原始分
   - 支持 rng 注入(测试可复现);smart-match API/正式生成/模板编辑器三调用点全受益
2. 素材使用次数口径修复:
   - mark_asset_used_for_generation 新增 times 参数,按成片实际渲染片段引用次数累加
   - worker 回写从 task.asset_ids(请求列表,含未被plan选用的素材)改为
     统计最终成片 plan 的 edit_plan_clips(同素材多片段复用按片段数累加)
   - 抽 _count_plan_clip_asset_usage/_record_rendered_asset_usage 纯函数(可单测)
   - plan 无有效片段时兜底 task.asset_ids 单次计数;单素材失败不阻断其他
3. 测试:22 新测试(噪声 10 + 回写计数 12);旧确定性排序断言注入零噪声 rng;
   修复 test_distribute_assets 预存在 flaky(shuffle 未被零噪声 patch 覆盖)
2026-09-06 19:48:08 +08:00
xiaoxia 52664f7168 fix(auth): 默认昵称改小虾同学 + 登录不覆盖存量昵称 + 中文昵称链路守护 (#1746)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 27s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 34s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m50s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m57s
CI/CD Pipeline / Integration Tests (push) Successful in 2m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
CI/CD Pipeline / Validate - Style (push) Successful in 2m27s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m9s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m32s
AI Code Review / AI Code Review (pull_request) Successful in 4m16s
CI/CD Pipeline / Validate - Security (push) Successful in 4m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m39s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m49s
CI/CD Pipeline / Unit Tests (push) Failing after 7m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 236h52m16s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 236h52m20s
CI/CD Pipeline / Deploy Production (push) Failing after 236h52m16s
CI/CD Pipeline / Build Production Web Image (push) Failing after 236h52m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 236h57m2s
CI/CD Pipeline / Build Production API Image (push) Failing after 236h52m20s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 236h59m8s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 236h59m10s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 236h59m30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 236h59m35s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 236h59m42s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 236h59m38s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 236h59m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 236h59m40s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 237h0m8s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 236h59m40s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 237h0m9s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 237h0m17s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 237h0m17s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 237h0m19s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 237h0m17s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 237h0m19s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 237h0m18s
CI/CD Pipeline / Frontend Lint (push) Failing after 237h0m19s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 237h0m23s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 237h0m23s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 237h0m24s
CI/CD Pipeline / PR Build API Image (push) Failing after 237h0m24s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 237h0m25s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 237h0m26s
CI/CD Pipeline / CI Gate (push) Failing after 237h26m57s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 237h33m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 237h34m19s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 237h34m21s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 237h34m46s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 237h34m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 237h35m0s
CI/CD Pipeline / PR Build Web Image (push) Failing after 237h35m1s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 18:23:55 +08:00
xiaoxia 0469272bd6 fix(#1743): 批量变体独立选片——完整重跑单视频选片+批次20%重叠避让+查重超阈重渲+封面独立 (#1745)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging API Image (push) Successful in 38s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m59s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m24s
CI/CD Pipeline / Integration Tests (push) Successful in 2m40s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m16s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (push) Successful in 3m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Validate - Security (push) Successful in 5m28s
AI Code Review / AI Code Review (pull_request) Failing after 5m35s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m22s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 5m6s
CI/CD Pipeline / Unit Tests (push) Successful in 8m54s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 237h7m46s
CI/CD Pipeline / Deploy Production (push) Failing after 237h7m46s
CI/CD Pipeline / CI Gate (push) Failing after 237h7m57s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 237h7m57s
CI/CD Pipeline / Build Production API Image (push) Failing after 237h7m58s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 237h13m3s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 237h14m5s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 237h14m6s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 237h14m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 237h14m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 237h15m1s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 237h16m28s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 237h15m5s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 237h16m28s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 237h16m47s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 237h16m47s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 237h16m51s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 237h16m51s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 237h16m51s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 237h16m52s
CI/CD Pipeline / Frontend Lint (push) Failing after 237h16m54s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 237h16m52s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 237h16m55s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 237h16m53s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 237h16m56s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 237h16m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 237h16m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 237h16m55s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 237h16m57s
CI/CD Pipeline / PR Build API Image (push) Failing after 237h16m56s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 237h16m58s
CI/CD Pipeline / Build Production Web Image (push) Failing after 237h42m33s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 237h48m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 237h49m29s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 237h49m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 237h51m4s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 237h51m23s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 237h51m27s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 18:07:24 +08:00
xiaoxia 6638f8b29e feat(#1741): 批量预览卡片缩小至3/5 + 每个视频播放都有声音(配音全挂载+播放互斥+静音按钮) (#1742)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m57s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / Validate - Style (push) Successful in 2m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 2m13s
AI Code Review / AI Code Review (pull_request) Failing after 5m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m32s
CI/CD Pipeline / Validate - Security (push) Successful in 5m45s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m51s
CI/CD Pipeline / Unit Tests (push) Successful in 8m12s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 22m33s
CI/CD Pipeline / Canary Release to Production (push) Failing after 239h17m11s
CI/CD Pipeline / Deploy Production (push) Failing after 239h34m24s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 239h34m26s
CI/CD Pipeline / Build Production Web Image (push) Failing after 239h34m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 239h34m27s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 239h39m13s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 239h40m50s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 239h40m55s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 239h40m57s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 239h40m59s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 239h40m58s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 239h41m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 239h41m7s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 239h41m2s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 239h41m15s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 239h41m17s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 239h42m3s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 239h42m3s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 239h42m35s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 239h42m33s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 239h42m36s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 239h42m34s
CI/CD Pipeline / Frontend Lint (push) Failing after 239h42m38s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 239h42m34s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 239h42m43s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 239h42m44s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 239h42m37s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 239h42m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 239h42m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 239h42m46s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 239h42m47s
CI/CD Pipeline / CI Gate (push) Failing after 240h9m2s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h15m31s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 240h15m44s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h16m38s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 240h17m10s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h17m19s
CI/CD Pipeline / PR Build Web Image (push) Failing after 240h17m20s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-06 15:41:36 +08:00
xiaoxia 794793f992 Merge pull request 'fix(auth): 微信 unionid 账号打通逻辑修复(staging 部署版,含 profile_completed)' (#1740) from fix/wechat-unionid-linking-develop into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 13s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 53s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m10s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 44s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m56s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (push) Successful in 2m33s
CI/CD Pipeline / Integration Tests (push) Successful in 2m32s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m22s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
CI/CD Pipeline / Validate - Security (push) Successful in 5m32s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m57s
AI Code Review / AI Code Review (pull_request) Failing after 6m12s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m11s
CI/CD Pipeline / Unit Tests (push) Successful in 8m39s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 240h6m14s
CI/CD Pipeline / CI Gate (push) Failing after 240h6m18s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 240h6m18s
CI/CD Pipeline / Build Production API Image (push) Failing after 240h6m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 240h11m29s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 240h12m42s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 240h12m53s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 240h12m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 240h12m57s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 240h12m58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 240h13m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 240h13m9s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 240h13m12s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 240h13m9s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 240h14m22s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 240h14m22s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 240h14m49s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 240h14m50s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 240h14m51s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 240h14m52s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 240h14m53s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 240h14m56s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 240h14m57s
CI/CD Pipeline / Frontend Lint (push) Failing after 240h14m57s
CI/CD Pipeline / PR Build Web Image (push) Failing after 240h15m1s
CI/CD Pipeline / PR Build API Image (push) Failing after 240h15m2s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 240h15m4s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 240h15m4s
CI/CD Pipeline / Deploy Production (push) Failing after 240h40m50s
CI/CD Pipeline / Build Production Web Image (push) Failing after 240h40m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 240h47m30s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 240h47m40s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 240h47m47s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 240h48m59s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 240h49m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 240h49m28s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 240h49m33s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 240h49m37s
2026-09-06 15:09:18 +08:00
196 changed files with 14541 additions and 2797 deletions
@@ -0,0 +1,72 @@
"""Projects is_default + partial unique index for idempotent default project (Issue #1775)
Revision ID: 069_project_is_default
Revises: 068_user_profile_completed
Create Date: 2026-09-08
背景:
小程序端 getOrCreateDefaultProject 在重试/并发/前端重复调用下,
仅靠应用层"先查再插"不保证幂等,会给同一用户重复创建默认项目。
改动:
1. projects 表新增 is_default 布尔列(默认 false)
2. 部分唯一索引 uq_projects_owner_default:(owner_user_id) WHERE is_default = true
—— 保证每个用户至多一个默认项目
3. 存量数据回填:把名为"默认项目"的存量项目按创建时间最早者标记为 is_default=true
(只标记不删除;存量重复项目的清理另行确认后单独执行)
注意:部分唯一索引依赖 PostgreSQL,不支持 downgrade 到其他方言。
"""
import sqlalchemy as sa
from alembic import op
revision = "069_project_is_default"
down_revision = "068_user_profile_completed"
branch_labels = None
depends_on = None
def upgrade() -> None:
# 1. 新增 is_default 列
op.add_column(
"projects",
sa.Column(
"is_default",
sa.Boolean(),
nullable=False,
server_default=sa.text("false"),
),
)
# 2. 存量回填:每个拥有"默认项目"的用户,只把最早创建的那一个标记为默认。
# 用 ROW_NUMBER() 取每组第一条;非"默认项目"命名的项目不标记(保守,不动用户自建项目)。
op.execute("""
UPDATE projects p
SET is_default = true
WHERE p.id IN (
SELECT id FROM (
SELECT id,
ROW_NUMBER() OVER (
PARTITION BY owner_user_id
ORDER BY created_at ASC, id ASC
) AS rn
FROM projects
WHERE name = '默认项目'
) t
WHERE t.rn = 1
)
""")
# 3. 部分唯一索引:每用户至多一个默认项目(只约束 is_default = true 的行)
op.execute("""
CREATE UNIQUE INDEX uq_projects_owner_default
ON projects (owner_user_id)
WHERE is_default = true
""")
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS uq_projects_owner_default")
op.drop_column("projects", "is_default")
+48
View File
@@ -0,0 +1,48 @@
"""Add scripts table for oral broadcast script library (Issue #1795)
Revision ID: 070_add_scripts
Revises: 069_project_is_default
Create Date: 2026-09-08
新建 scripts 表,支持口播文案 CRUD + 分段存储。
"""
import sqlalchemy as sa
from alembic import op
revision = "070_add_scripts"
down_revision = "069_project_is_default"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"scripts",
sa.Column("id", sa.String(36), nullable=False),
sa.Column("user_id", sa.String(36), nullable=False),
sa.Column("title", sa.String(255), nullable=False),
sa.Column("content", sa.Text(), nullable=False, server_default=""),
sa.Column("segments", sa.JSON(), nullable=False, server_default="[]"),
sa.Column("tags", sa.JSON(), nullable=False, server_default="[]"),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.Column(
"updated_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_scripts_user_id", "scripts", ["user_id"])
def downgrade() -> None:
op.drop_index("ix_scripts_user_id", table_name="scripts")
op.drop_table("scripts")
+12
View File
@@ -11,10 +11,12 @@ from app.api.routes.feature_flags import router as feature_flags_router
from app.api.routes.generation_cover import router as generation_cover_router
from app.api.routes.generation_preview import router as generation_preview_router
from app.api.routes.generation_tasks import router as generation_tasks_router
from app.api.routes.generation_variant_plans import router as generation_variant_plans_router
from app.api.routes.health import router as health_check_router
from app.api.routes.ingest_jobs import router as ingest_jobs_router
from app.api.routes.internal_render import router as internal_render_router
from app.api.routes.projects import router as projects_router
from app.api.routes.scripts import router as scripts_router
from app.api.routes.share import router as share_router
from app.api.routes.subscription import router as subscription_router
from app.api.routes.tags import router as tags_router
@@ -99,6 +101,11 @@ api_router.include_router(
prefix="/generation",
tags=["Generation"],
)
api_router.include_router(
generation_variant_plans_router,
prefix="/generation",
tags=["Generation"],
)
api_router.include_router(
generation_cover_router,
prefix="/generation",
@@ -165,3 +172,8 @@ api_router.include_router(
internal_render_router,
tags=["Internal"],
)
api_router.include_router(
scripts_router,
prefix="/scripts",
tags=["ScriptLibrary"],
)
+5 -24
View File
@@ -20,7 +20,7 @@ from packages.application import (
GetProjectUseCase,
ListAssetLibrariesUseCase,
)
from packages.domain import AssetLibrary, AssetLibraryKind
from packages.domain import AssetLibraryKind
from ._helpers import check_project_access
@@ -120,30 +120,11 @@ def ensure_default_library(
kind = AssetLibraryKind(request.kind)
# 查找该项目下同 kind 的素材库,返回第一个
existing = asset_library_repository.find_by_project(request.project_id)
for lib in existing:
if lib.kind == kind:
return _to_asset_library_response(lib)
# 不存在 → 自动创建
import uuid
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
# Issue #1775: 幂等获取/创建——依赖唯一约束 uq_asset_libraries_project_kind,
# 并发创建冲突时回滚重查返回已有记录,不再依赖应用层"先查后插",也不会 500。
default_name = _DEFAULT_LIBRARY_NAMES.get(request.kind, f"{request.kind}素材库")
library = AssetLibrary(
id=str(uuid.uuid4()),
project_id=request.project_id,
name=default_name,
kind=kind,
asset_count=0,
total_size=0,
created_at=now,
updated_at=now,
)
created = asset_library_repository.create(library)
return _to_asset_library_response(created)
library = asset_library_repository.get_or_create_default_library(request.project_id, kind, name=default_name)
return _to_asset_library_response(library)
@router.delete("/{library_id}", status_code=status.HTTP_204_NO_CONTENT, response_class=Response)
+84 -4
View File
@@ -75,6 +75,86 @@ class GenerateCoverResponse(BaseModel):
# ── Route ────────────────────────────────────────────────────────────────
def _select_best_frame_from_snapshots(
snapshots: list[dict], plan_id: str
) -> str:
"""从 MediaKit 抽帧结果中,通过质量评分选出最佳帧。
降级策略:cv2 不可用或评分失败时,返回第一帧。
Args:
snapshots: MediaKit 返回的帧列表 [{"image_url": str, ...}, ...]
plan_id: 计划 ID(日志用)
Returns:
最佳帧的 image_url,或空字符串
"""
if not snapshots:
return ""
if len(snapshots) == 1:
return snapshots[0].get("image_url") or snapshots[0].get("url") or ""
try:
import tempfile
import httpx
from packages.shared.cover_frame_scorer import score_frames
scored_candidates = []
for snap in snapshots:
url = snap.get("image_url") or snap.get("url") or ""
if not url:
continue
# 下载帧到临时文件进行评分
try:
resp = httpx.get(url, timeout=15, follow_redirects=True)
resp.raise_for_status()
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
tmp.write(resp.content)
tmp_path = tmp.name
scored_candidates.append({"image_path": tmp_path, "url": url})
except Exception:
# 下载失败的帧跳过,给默认低分
scored_candidates.append({"image_path": None, "url": url, "score": 0.0})
if not scored_candidates:
return snapshots[0].get("image_url") or snapshots[0].get("url") or ""
scored = score_frames(scored_candidates)
best = scored[0] if scored else None
best_url = best.get("url", "") if best else ""
best_score = best.get("score", 0.0) if best else 0.0
logger.info(
"[封面生成] 帧质量评分完成: plan_id=%s candidates=%d best_score=%.1f",
plan_id,
len(scored_candidates),
best_score,
)
# 清理临时文件
for c in scored_candidates:
path = c.get("image_path")
if path:
try:
from pathlib import Path
Path(path).unlink(missing_ok=True)
except Exception:
pass
return best_url
except Exception:
logger.warning(
"[封面生成] 帧质量评分失败,使用第一帧: plan_id=%s",
plan_id,
exc_info=True,
)
return snapshots[0].get("image_url") or snapshots[0].get("url") or ""
def _persist_cover_frame(
frame_url: str,
plan_id: str,
@@ -652,13 +732,13 @@ def generate_cover(
snapshots = mk_client.extract_frames(
video_url=primary_video_url,
strategy="SpecifiedFrames",
max_frames=1,
max_frames=5, # 抽 5 帧,通过质量评分选最佳
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 ""
raw = _select_best_frame_from_snapshots(snapshots, plan_id)
if raw:
cover_url_from_task = _persist_cover_frame(raw, plan_id)
logger.info(
@@ -715,13 +795,13 @@ def generate_cover(
snapshots = mk_client.extract_frames(
video_url=src_url,
strategy="SpecifiedFrames",
max_frames=1,
max_frames=5, # 抽 5 帧,通过质量评分选最佳
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 ""
raw = _select_best_frame_from_snapshots(snapshots, plan_id)
if raw:
cover_url_from_task = _persist_cover_frame(
raw,
+89 -14
View File
@@ -390,7 +390,6 @@ def create_preview_generation_task(
template_id=request.template_id,
asset_ids=list(request.asset_ids),
title_ids=list(request.title_ids),
voice_ids=list(request.voice_ids),
created_by_user_id=user_id,
source_edit_plan_id=request.source_edit_plan_id,
asset_select_mode="",
@@ -420,42 +419,118 @@ def create_preview_generation_task(
logger.error("[预览生成] 创建失败: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="创建预览生成任务失败,请稍后再试") from e
# ── 克隆独立变体 plan:N 个预览全部克隆(预览不污染源 plan)──
# 源 plan 不存在(无编辑历史)时各任务走自身随机选片流程,不克隆。
# ── 独立变体 plan(#1743)──
# count=1:克隆源 plan(预览不污染源 plan,仅起点重算),行为与旧版一致;
# count>1:变体 0 保留源 plan,变体 1..N-1 用 reselect_plan_for_variant 完整
# 重跑单视频选片(素材洗牌+镜头洗牌+起点随机+跨变体避让+批次 20% 重叠重选),
# 所见即所得——预览变体差异即正式成片差异。
source_plan_id = created_tasks[0].source_edit_plan_id if created_tasks else ""
if source_plan_id:
# #1749:各变体配音解析(严格守卫已在 schema;此处取每变体 voice 查时长)+ 时长分配
def _preview_voice_durations() -> list[float]:
try:
from packages.domain.variant_voice_resolver import resolve_variant_voice_ids
voices = resolve_variant_voice_ids(
count=count,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
except Exception:
logger.warning("[预览生成] 配音解析失败(按无配音处理)", exc_info=True)
return [0.0] * count
try:
from app.api.routes.generation_tasks import _query_voice_durations
return _query_voice_durations(db, voices)
except Exception:
return [0.0] * count
voice_durations = _preview_voice_durations()
if source_plan_id and count == 1:
# 单预览:克隆一份(原逻辑)+ 配音时长分配
try:
from app.services.edit_plan_service import EditPlanService
_plan_svc = EditPlanService(db)
for variant_index in range(count):
variant_plan = _plan_svc.clone_plan_for_variant(
source_plan_id,
created_by_user_id=user_id,
name_suffix="预览变体",
)
if voice_durations and voice_durations[0] > 0:
try:
_plan_svc.apply_voice_duration_to_plan(variant_plan.id, voice_durations[0])
except Exception:
logger.exception("[预览生成] 变体0 配音分配失败(不阻断): plan=%s", variant_plan.id)
variant_plan_ids.append(variant_plan.id)
except Exception as e:
logger.error("[预览生成] 克隆预览 plan 异常: %s", e, exc_info=True)
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览计划创建失败")
raise HTTPException(
status_code=500,
detail="创建预览任务失败:无法生成独立剪辑计划,请重试",
) from e
elif source_plan_id and count > 1:
try:
from app.services.edit_plan_service import EditPlanService
_plan_svc = EditPlanService(db)
# #1749:变体 0 也 clone(不污染源 plan)+ 配音分配;变体 1..N-1 独立选片
_plan0 = _plan_svc.clone_plan_for_variant(
source_plan_id,
created_by_user_id=user_id,
name_suffix="预览变体1",
)
if voice_durations and voice_durations[0] > 0:
try:
_plan_svc.apply_voice_duration_to_plan(_plan0.id, voice_durations[0])
except Exception:
logger.exception("[预览生成] 变体0 配音分配失败(不阻断): plan=%s", _plan0.id)
variant_plan_ids.append(_plan0.id)
batch_asset_pool = list(dict.fromkeys(request.asset_ids or []))
for variant_index in range(1, count):
last_err: Exception | None = None
variant_plan = None
for _attempt in range(2): # 1 次重试,抗 DB 瞬时抖动
try:
variant_plan = _plan_svc.clone_plan_for_variant(
variant_plan = _plan_svc.reselect_plan_for_variant(
source_plan_id,
batch_asset_pool,
created_by_user_id=user_id,
name_suffix=f"预览变体{variant_index + 1}" if count > 1 else "预览变体",
name_suffix=f"预览变体{variant_index + 1}",
voice_duration=(
voice_durations[variant_index] if variant_index < len(voice_durations) else 0.0
),
)
break
except Exception as clone_err: # noqa: PERF203
last_err = clone_err
except ValueError as ve:
logger.warning("[预览生成] 变体独立选片失败(素材不足): %s", ve)
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览变体选片失败")
raise HTTPException(
status_code=400,
detail=f"批量预览第 {variant_index + 1} 个视频无法独立选片:{ve}。"
"请增加素材库中的视频素材后重试。",
) from ve
except Exception as reselection_err: # noqa: PERF203
last_err = reselection_err
logger.warning(
"[预览生成] 克隆变体 plan 失败(尝试%d/2): variant=%d error=%s",
"[预览生成] 变体独立选片失败(尝试%d/2): variant=%d error=%s",
_attempt + 1,
variant_index,
clone_err,
reselection_err,
exc_info=True,
)
if variant_plan is None:
logger.error(
"[预览生成] 克隆预览变体 plan 重试仍失败: variant=%d source=%s",
"[预览生成] 变体独立选片重试仍失败: variant=%d source=%s",
variant_index,
source_plan_id,
exc_info=last_err,
)
# 标记已创建任务失败
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览变体计划创建失败")
raise HTTPException(
@@ -466,7 +541,7 @@ def create_preview_generation_task(
except HTTPException:
raise
except Exception as e:
logger.error("[预览生成] 克隆变体 plan 异常: %s", e, exc_info=True)
logger.error("[预览生成] 变体 plan 生成异常: %s", e, exc_info=True)
for t in created_tasks:
_mark_task_failed(generation_task_repository, t, "预览变体计划创建失败")
raise HTTPException(
+225 -44
View File
@@ -57,6 +57,30 @@ def _variant_value(values: list[str], index: int, fallback: str = "") -> str:
return values[index] if index < len(values) else fallback
def _query_voice_durations(db: Session, voice_ids: list[str]) -> list[float]:
"""批量查询配音素材时长(秒),#1749 配音时长分配用。
逐项 try/float 硬化:MagicMock/异常/缺失 → 0.0(无配音不分配,不阻断)。
"""
ids = [v for v in dict.fromkeys(voice_ids or []) if v]
if not ids:
return []
try:
from packages.adapters.sqlalchemy_impl.models import AssetModel
rows = db.query(AssetModel.id, AssetModel.duration).filter(AssetModel.id.in_(ids)).all()
dur_map: dict[str, float] = {}
for row in rows:
try:
dur_map[row[0]] = float(row[1] or 0.0)
except (TypeError, ValueError):
dur_map[row[0]] = 0.0
return [dur_map.get(v, 0.0) for v in ids]
except Exception:
logger.warning("[生成任务] 配音时长查询失败(按无配音处理,不阻断)", exc_info=True)
return [0.0 for _ in ids]
def _to_generation_task_response(task) -> GenerationTaskResponse:
return GenerationTaskResponse(
id=task.id,
@@ -123,6 +147,7 @@ def _select_assets_from_library(
assets: list,
mode: str,
count: int,
rng=None,
) -> list[str]:
"""根据选取模式从素材库中选取 ready 状态的视频素材 ID。
@@ -130,6 +155,8 @@ def _select_assets_from_library(
assets: 素材库中所有素材(Asset 实体列表)
mode: 选取模式 — all=全部, smart=智能匹配(多维度评分+多样性)
count: 选取数量,0 表示全部(仅 smart 模式有效)
rng: 可选随机源(smart 模式排序噪声用),生产环境不传则内部随机;
测试可注入固定种子或零噪声随机源获得确定性结果。
Returns:
选中的素材 ID 列表
@@ -142,8 +169,9 @@ def _select_assets_from_library(
if mode == "smart":
# 智能匹配:统一使用 packages/domain/smart_match.py 的多维评分+多样性选取
# 评分维度:质量分(40%) + 时长适配(30%) + 新鲜度(20%) + 未使用加分(10%)
# 排序注入随机噪声(#1743):同分素材每次选出不同组合,从素材组合层面降重
limit = count if count > 0 else None
results = smart_select_assets(ready_video_assets, limit=limit, kind="video")
results = smart_select_assets(ready_video_assets, limit=limit, kind="video", rng=rng)
return [r.asset.id for r in results]
# 默认 all 模式:返回全部 ready 视频素材
@@ -432,55 +460,199 @@ def create_generation_task(
logger.info("画中画已下线,strategy_id %s → one_take", effective_strategy_id)
effective_strategy_id = "one_take"
# 批量生成时每个任务关联独立克隆 plan(片段起点重算),
# 禁止 N 条任务共用同一 source_edit_plan_id 导致片段一模一样。
# 在创建任何任务【之前】预克隆全部变体:克隆失败直接中断(此时无脏数据),
# 绝不静默退回共用源 plan(否则批量视频内容重复,违反去重诉求)。
# 批量生成(count>1):每个变体必须走与单视频完全相同的独立选片流程(#1743/#1749)。
# - 变体 0:clone 源 plan(不污染源 plan),变体 1..N-1 用 reselect_plan_for_variant
# 完整重跑选片(素材级去重:fresh 优先 → 受控复用 overlap≤20% → 短素材禁复用);
# - #1749:前端可回传 variant-plans 接口预生成的 plan_id(variant_plan_ids),直接复用;
# 回传 plan 仍按各变体配音幂等重分配段长(防 variant-plans 阶段未带配音/占位时长);
# - 配音时长:独立配音各自时长、统一配音同值,逐变体 apply_voice_duration_to_plan,
# 成片总时长=配音时长(素材短→末帧冻结,禁慢放/禁截配音);
# - count>1 但没有源 plan 时,不允许 N 个任务兜底共用同一 plan,直接 4xx 中断。
# 在创建任何任务【之前】预生成/校验全部变体 plan:失败直接中断(此时无脏数据)。
variant_plan_ids: list[str] = []
if count > 1 and request.source_edit_plan_id:
if count > 1:
from app.services.edit_plan_service import EditPlanService
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
_plan_svc = EditPlanService(db)
for task_index in range(1, count):
variant = None
last_err: Exception | None = None
for _attempt in range(2): # 1 次重试,抗 DB 瞬时抖动
try:
variant = _plan_svc.clone_plan_for_variant(
request.source_edit_plan_id,
created_by_user_id=user_id,
name_suffix=f"批量{task_index + 1}",
# 解析每变体配音(严格守卫:独立配音长度/缺值 → 400,禁静默 fallback)
try:
variant_voices = resolve_variant_voice_ids(
count=count,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
except VariantVoiceError as ve:
raise HTTPException(status_code=400, detail=str(ve)) from ve
# 各变体配音时长(查询硬化:异常 → 0.0 不阻断)
voice_durations = _query_voice_durations(db, variant_voices)
# 解析批量源 plan:优先前端传入;否则按 template_id + user 查最新(与单任务兜底同源)
batch_source_plan_id = request.source_edit_plan_id
if not batch_source_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_latest = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id,
EditPlanModel.created_by_user_id == user_id,
)
break
except Exception as clone_err: # noqa: PERF203
last_err = clone_err
logger.warning(
"[生成任务] 克隆变体 plan 失败(尝试%d/2): source=%s error=%s",
_attempt + 1,
request.source_edit_plan_id,
clone_err,
exc_info=True,
)
if variant is None:
logger.error(
"[生成任务] 克隆变体 plan 重试仍失败,中断批量创建: source=%s",
request.source_edit_plan_id,
exc_info=last_err,
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _latest:
batch_source_plan_id = _latest.id
except Exception:
logger.warning("[生成任务] 批量源 plan 解析失败", exc_info=True)
if not batch_source_plan_id and not request.variant_plan_ids:
# 无任何可用源 plan:批量变体无从选片,明确报错,严禁静默共用/同源
logger.error("[生成任务] 批量 count=%d 但无可编辑计划(无 source_edit_plan_id/template plan)", count)
raise HTTPException(
status_code=400,
detail="批量生成需要先完成预览生成(缺少剪辑计划)。请先生成预览后再批量创建。",
)
# 批次素材池:请求显式素材 + 库自动匹配素材(resolved_asset_ids)
batch_asset_pool = list(dict.fromkeys(resolved_asset_ids or []))
if request.variant_plan_ids:
# ① 前端回传 variant-plans 预生成结果:直接复用(轻量选片接口已建好 plan)
if len(request.variant_plan_ids) != count:
raise HTTPException(
status_code=500,
detail="创建批量任务失败:无法生成独立剪辑计划,请重试",
) from last_err
variant_plan_ids.append(variant.id)
status_code=400,
detail=f"variant_plan_ids 数量({len(request.variant_plan_ids)})与视频数量({count})不一致",
)
# 校验归属权
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
for _pid in request.variant_plan_ids:
_pm = db.query(EditPlanModel).filter(EditPlanModel.id == _pid).first()
if _pm is None:
raise HTTPException(status_code=400, detail=f"剪辑计划不存在: {_pid}")
if _pm.created_by_user_id and _pm.created_by_user_id != user_id:
raise HTTPException(status_code=403, detail=f"无权使用剪辑计划: {_pid}")
variant_plan_ids = list(request.variant_plan_ids)
else:
# ② 服务端选片:变体 0 clone 源 plan(不污染源 plan)
try:
_plan0 = _plan_svc.clone_plan_for_variant(
batch_source_plan_id,
created_by_user_id=user_id,
name_suffix="批量1",
)
except Exception as clone_err:
logger.error("[生成任务] 变体0 clone 失败: %s", clone_err, exc_info=True)
raise HTTPException(
status_code=500, detail="创建批量任务失败:无法生成独立剪辑计划,请重试"
) from clone_err
variant_plan_ids.append(_plan0.id)
# 变体 1..N-1 独立选片
for task_index in range(1, count):
variant = None
last_err: Exception | None = None
for _attempt in range(2): # 1 次重试,抗 DB 瞬时抖动
try:
variant = _plan_svc.reselect_plan_for_variant(
batch_source_plan_id,
batch_asset_pool,
created_by_user_id=user_id,
name_suffix=f"批量{task_index + 1}",
voice_duration=voice_durations[task_index] if task_index < len(voice_durations) else 0.0,
)
break
except ValueError as ve:
# 素材不足等可预期错误:不重试,直接中断并给出明确提示
logger.warning("[生成任务] 变体独立选片失败(素材不足): %s", ve)
raise HTTPException(
status_code=400,
detail=f"批量生成第 {task_index + 1} 个视频无法独立选片:{ve}。"
"请增加素材库中的视频素材后重试。",
) from ve
except Exception as reselection_err: # noqa: PERF203
last_err = reselection_err
logger.warning(
"[生成任务] 变体独立选片失败(尝试%d/2): source=%s error=%s",
_attempt + 1,
batch_source_plan_id,
reselection_err,
exc_info=True,
)
if variant is None:
logger.error(
"[生成任务] 变体独立选片重试仍失败,中断批量创建: source=%s",
batch_source_plan_id,
exc_info=last_err,
)
raise HTTPException(
status_code=500,
detail="创建批量任务失败:无法生成独立剪辑计划,请重试",
) from last_err
variant_plan_ids.append(variant.id)
# ③ 配音时长分配(回传 plan / clone 变体0 均需幂等分配;reselect 已在选片时分配)
for _vi, _pid in enumerate(variant_plan_ids):
_vd = voice_durations[_vi] if _vi < len(voice_durations) else 0.0
if _vd > 0:
try:
_plan_svc.apply_voice_duration_to_plan(_pid, _vd)
except Exception:
logger.exception("[生成任务] 变体%d 配音时长分配失败(不阻断): plan=%s", _vi, _pid)
# N=1 正式生成:渲染侧全局慢放兜底已删除(#1749),enqueue 前也必须按配音分配段长
if count == 1 and not request.is_preview:
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
try:
_voices = resolve_variant_voice_ids(
count=1,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
_single_vd: list[float] = _query_voice_durations(db, _voices)
_single_dur = _single_vd[0] if _single_vd else 0.0
_single_plan = request.source_edit_plan_id
if not _single_plan and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_latest = (
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 _latest:
_single_plan = _latest.id
except Exception:
logger.warning("[生成任务] 单任务源 plan 解析失败", exc_info=True)
if _single_dur > 0 and _single_plan:
from app.services.edit_plan_service import EditPlanService
try:
EditPlanService(db).apply_voice_duration_to_plan(_single_plan, _single_dur)
except Exception:
logger.exception("[生成任务] N=1 配音时长分配失败(不阻断): plan=%s", _single_plan)
except VariantVoiceError as ve:
raise HTTPException(status_code=400, detail=str(ve)) from ve
except Exception:
logger.exception("[生成任务] N=1 配音分配兜底异常(不阻断)")
try:
for task_index in range(count):
# 第 1 条复用源 plan(保留用户编辑结果);其余使用预克隆的独立变体 plan。
# 无源 plan(source_edit_plan_id 为空)时无可克隆对象,variant_plan_ids
# 为空列表:各任务走自身随机选片流程,不做索引访问(防 IndexError)
effective_plan_id = request.source_edit_plan_id
if task_index > 0 and variant_plan_ids:
effective_plan_id = variant_plan_ids[task_index - 1]
# #1749:count>1 时每个变体(含变体0)都关联各自独立 plan(clone/reselect/variant-plans)。
if count > 1 and variant_plan_ids:
effective_plan_id = variant_plan_ids[task_index]
else:
effective_plan_id = request.source_edit_plan_id
# 变体级独立配置:titles[]/voice_library_ids[]/cover_urls[]
# 长度1=所有变体共用,长度=count=每个变体独立,空数组=回退单值字段
@@ -500,7 +672,6 @@ def create_generation_task(
template_id=request.template_id,
asset_ids=resolved_asset_ids,
title_ids=request.title_ids,
voice_ids=request.voice_ids,
created_by_user_id=user_id,
source_edit_plan_id=effective_plan_id,
asset_select_mode=request.asset_select_mode,
@@ -523,7 +694,19 @@ def create_generation_task(
try:
# 兜底关联编辑计划:前端未传 source_edit_plan_id 时,
# 通过 template_id + user_id 在 DB 层直接查找最新的 plan。
# 必须在 enqueue 之前执行,避免 worker 读取时 source_edit_plan_id 为空(竞态条件)
# 必须在 enqueue 之前执行,避免 worker 读取时 source_edit_plan_id 为空(竞态条件)。
# #1743:批量(count>1)场景严禁兜底共用——变体 plan 已在上方预生成,
# 走到这里还缺 plan 说明预生成漏配,直接报错中断,不允许 N 任务关联同一 plan。
if not task.source_edit_plan_id and count > 1:
logger.error(
"[生成任务] 批量任务缺少独立 plan(禁止共用兜底): task_index=%d task_id=%s",
task_index,
task.id,
)
raise HTTPException(
status_code=500,
detail="创建批量任务失败:变体剪辑计划缺失,请重新预览后再批量生成。",
)
if not task.source_edit_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
@@ -690,7 +873,6 @@ def confirm_generation(
template_id=source_task.template_id,
asset_ids=source_task.asset_ids,
title_ids=source_task.title_ids,
voice_ids=source_task.voice_ids,
created_by_user_id=authenticated_user.user.id,
source_edit_plan_id=source_task.source_edit_plan_id or "",
asset_select_mode=source_task.asset_select_mode,
@@ -834,7 +1016,6 @@ def retry_generation_task(
template_id=task.template_id,
asset_ids=task.asset_ids,
title_ids=task.title_ids,
voice_ids=task.voice_ids,
created_by_user_id=user_id,
source_edit_plan_id=task.source_edit_plan_id or "",
asset_select_mode=getattr(task, "asset_select_mode", ""),
@@ -0,0 +1,178 @@
"""轻量选片接口 POST /generation/variant-plans(#1749)。
与正式生成共用同一套选片函数(EditPlanService.ensure_variant_plans →
clone_plan_for_variant / reselect_plan_for_variant → variant_plan_selector),
但**不建任务、不入队、不渲染**:
- 仅为 N 个变体创建/选好 EditPlan + clips,返回 plan_id 与片段列表;
- 前端确认后调正式生成接口回传 variant_plan_ids,直接复用这些 plan,
不再重复选片(回传后仍按各变体配音幂等重分配段长);
- 配音守卫:voice_library_ids 长度/缺值 → 400(variant_voice_resolver),
禁静默 fallback;
- 素材不足等选片失败 → 400(与正式生成同口径);除此之外不报错打断。
"""
from __future__ import annotations
import logging
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, model_validator
from sqlalchemy.orm import Session
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
logger = logging.getLogger(__name__)
router = APIRouter()
class VariantPlanRequest(BaseModel):
"""轻量选片请求体(与前端 variantPlans.ts 契约一致)。"""
template_id: str = Field(default="", description="模板 ID(无 source_edit_plan_id 时用于查找骨架 plan)")
asset_ids: list[str] = Field(default_factory=list, description="批次素材池")
count: int = Field(default=1, ge=1, le=50, description="变体数量")
source_edit_plan_id: str = Field(default="", description="源剪辑计划 ID(优先)")
# 配音(可选;传独立配音时严格守卫)
voice_library_id: str = Field(default="", description="统一配音 ID")
voice_library_ids: list[str] = Field(default_factory=list, description="独立配音 ID 列表(长度须=count)")
@model_validator(mode="after")
def _validate(self) -> "VariantPlanRequest":
if not self.template_id.strip() and not self.source_edit_plan_id.strip():
raise ValueError("template_id 与 source_edit_plan_id 至少需要提供一个")
try:
resolve_variant_voice_ids(
count=self.count,
voice_library_id=self.voice_library_id,
voice_library_ids=self.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise ValueError(str(exc)) from exc
return self
class VariantPlanItem(BaseModel):
variant_index: int
plan_id: str
clips: list[dict[str, Any]] = Field(default_factory=list)
class VariantPlanResponse(BaseModel):
items: list[VariantPlanItem]
total: int
@router.post("/variant-plans", response_model=VariantPlanResponse)
def create_variant_plans(
request: VariantPlanRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
db: Session = Depends(get_db_session),
) -> VariantPlanResponse:
"""轻量选片:为 N 个变体创建独立 EditPlan + clips,不建任务/不渲染。
Returns:
200 + {items: [{variant_index, plan_id, clips}], total}
"""
user_id = authenticated_user.user.id
# 配音严格守卫(schema 已校验,此处复用解析取每变体配音)
try:
voices = resolve_variant_voice_ids(
count=request.count,
voice_library_id=request.voice_library_id,
voice_library_ids=request.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
# 解析源 plan:显式传入优先;否则按 template_id + user 查最新
source_plan_id = request.source_edit_plan_id.strip()
if not source_plan_id and request.template_id.strip():
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_latest = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id.strip(),
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _latest:
source_plan_id = _latest.id
except Exception:
logger.warning("[variant-plans] 源 plan 解析失败", exc_info=True)
if not source_plan_id:
raise HTTPException(
status_code=400,
detail="缺少剪辑计划:请先完成一次预览生成(或传入 source_edit_plan_id)后再试。",
)
# 配音时长(硬化:异常 → 0.0 不阻断选片)
try:
from app.api.routes.generation_tasks import _query_voice_durations
voice_durations = _query_voice_durations(db, voices)
except Exception:
logger.warning("[variant-plans] 配音时长查询失败(按占位段长选片)", exc_info=True)
voice_durations = [0.0] * request.count
from app.services.edit_plan_service import EditPlanService
svc = EditPlanService(db)
try:
plan_ids = svc.ensure_variant_plans(
source_plan_id,
request.count,
list(dict.fromkeys(request.asset_ids or [])),
created_by_user_id=user_id,
voice_durations=voice_durations,
)
except ValueError as ve:
# 素材池为空/时长全未知等可预期错误 → 400(与正式生成同口径)
logger.warning("[variant-plans] 选片失败: %s", ve)
raise HTTPException(status_code=400, detail=f"变体选片失败:{ve}。请增加素材后重试。") from ve
except HTTPException:
raise
except Exception as e:
logger.error("[variant-plans] 选片异常: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="选片失败,请稍后重试") from e
# 组装 clips 响应
items: list[VariantPlanItem] = []
for idx, pid in enumerate(plan_ids):
clips = svc.list_clips(pid)
clip_dicts = [
{
"id": c.id,
"order": c.order,
"asset_id": c.asset_id,
"start_time": float(c.start_time or 0.0),
"duration": float(c.duration or 0.0),
"clip_type": c.clip_type,
"transition_effect": c.transition_effect,
"transition_duration": float(c.transition_duration or 0.0),
"playback_speed": float(c.playback_speed or 1.0),
"text_content": c.text_content or "",
"status": c.status or "ready",
}
for c in clips
]
items.append(VariantPlanItem(variant_index=idx, plan_id=pid, clips=clip_dicts))
logger.info(
"[variant-plans] 轻量选片完成: user=%s source=%s count=%d plans=%d",
user_id,
source_plan_id,
request.count,
len(plan_ids),
)
return VariantPlanResponse(items=items, total=len(items))
+41 -1
View File
@@ -1,13 +1,14 @@
from typing import Any
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_project_repository
from app.dependencies import get_asset_library_repository, get_project_repository
from app.schemas.project import (
CreateProjectRequest,
ListProjectsResponse,
ProjectResponse,
)
from fastapi import APIRouter, Depends, HTTPException, Response, status
from pydantic import BaseModel
from packages.application import (
CreateProjectCommand,
@@ -16,10 +17,20 @@ from packages.application import (
GetProjectUseCase,
ListProjectsUseCase,
)
from packages.domain import AssetLibraryKind
router = APIRouter()
class DefaultContextResponse(BaseModel):
"""幂等默认上下文响应(Issue #1775):默认项目 + 各类型默认素材库 ID。"""
project_id: str
image_library_id: str
video_library_id: str
voice_library_id: str
def _to_project_response(item) -> ProjectResponse:
return ProjectResponse(
id=item.id,
@@ -72,6 +83,35 @@ def create_project(
return _to_project_response(project)
@router.post("/ensure-default", response_model=DefaultContextResponse)
def ensure_default_project_and_libraries(
authenticated_user: AuthenticatedUser = Depends(get_current_user),
project_repository: Any = Depends(get_project_repository),
asset_library_repository: Any = Depends(get_asset_library_repository),
) -> DefaultContextResponse:
"""幂等获取/创建当前用户的默认项目和三类默认素材库(Issue #1775)。
- 同一用户永远只有一个默认项目(部分唯一索引 uq_projects_owner_default)
- 同一项目同 kind 永远只有一个默认素材库(唯一约束 uq_asset_libraries_project_kind)
- 并发调用/失败重试:唯一约束冲突时返回已存在记录,不报 500
- 项目和素材库的创建各自在仓储事务内幂等,冲突回滚后重查返回同一条
"""
user_id = authenticated_user.user.id
project = project_repository.get_or_create_default_project(user_id)
libraries = {}
for kind in (AssetLibraryKind.VIDEO, AssetLibraryKind.VOICE, AssetLibraryKind.IMAGE):
library = asset_library_repository.get_or_create_default_library(project.id, kind)
libraries[kind] = library.id
return DefaultContextResponse(
project_id=project.id,
image_library_id=libraries[AssetLibraryKind.IMAGE],
video_library_id=libraries[AssetLibraryKind.VIDEO],
voice_library_id=libraries[AssetLibraryKind.VOICE],
)
@router.delete("/{project_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_project(
project_id: str,
+123
View File
@@ -0,0 +1,123 @@
"""Script (口播文案库) CRUD routes — Issue #1795."""
from __future__ import annotations
from typing import Optional
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_db_session
from app.schemas.script import (
CreateScriptRequest,
ScriptListResponse,
ScriptResponse,
ScriptSegment,
UpdateScriptRequest,
)
from app.services.script_service import ScriptNotFoundError, ScriptService
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
from sqlalchemy.orm import Session
router = APIRouter()
def _get_service(session: Session = Depends(get_db_session)) -> ScriptService:
return ScriptService(session)
def _to_response(script) -> ScriptResponse:
segments = script.segments or []
return ScriptResponse(
id=script.id,
user_id=script.user_id,
title=script.title,
content=script.content,
segments=[
ScriptSegment(text=s.get("text", ""), duration=s.get("duration")) if isinstance(s, dict) else s
for s in segments
],
tags=script.tags or [],
created_at=script.created_at,
updated_at=script.updated_at,
)
@router.get("", response_model=ScriptListResponse)
def list_scripts(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
tag: Optional[str] = Query(None, description="按标签筛选"),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
svc: ScriptService = Depends(_get_service),
) -> ScriptListResponse:
user_id = authenticated_user.user.id
items, total = svc.list_scripts(user_id, skip=skip, limit=limit, tag=tag)
return ScriptListResponse(
items=[_to_response(i) for i in items],
total=total,
)
@router.post("", response_model=ScriptResponse, status_code=status.HTTP_201_CREATED)
def create_script(
request: CreateScriptRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
svc: ScriptService = Depends(_get_service),
) -> ScriptResponse:
user_id = authenticated_user.user.id
script = svc.create_script(
user_id=user_id,
title=request.title,
content=request.content,
segments=[s.model_dump() for s in request.segments],
tags=request.tags,
)
return _to_response(script)
@router.get("/{script_id}", response_model=ScriptResponse)
def get_script(
script_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
svc: ScriptService = Depends(_get_service),
) -> ScriptResponse:
user_id = authenticated_user.user.id
try:
script = svc.get_script(script_id, user_id)
except ScriptNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found") from exc
return _to_response(script)
@router.put("/{script_id}", response_model=ScriptResponse)
def update_script(
script_id: str,
request: UpdateScriptRequest,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
svc: ScriptService = Depends(_get_service),
) -> ScriptResponse:
user_id = authenticated_user.user.id
try:
script = svc.update_script(
script_id=script_id,
user_id=user_id,
title=request.title,
content=request.content,
segments=[s.model_dump() for s in request.segments] if request.segments is not None else None,
tags=request.tags,
)
except ScriptNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found") from exc
return _to_response(script)
@router.delete("/{script_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None, response_class=Response)
def delete_script(
script_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
svc: ScriptService = Depends(_get_service),
) -> Response:
user_id = authenticated_user.user.id
deleted = svc.delete_script(script_id, user_id)
if not deleted:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Script not found")
return
+5
View File
@@ -106,6 +106,10 @@ def list_templates(
tag: str | None = Query(None, description="按标签筛选"),
keyword: str | None = Query(None, description="按名称关键词搜索"),
mode: str | None = Query(None, description="按剪辑模式筛选"),
valid_only: bool = Query(
False,
description="仅返回已配置片段的模板(剪辑页传 true;模板编辑器不传,可查看全部模板含草稿)",
),
authenticated_user: AuthenticatedUser = Depends(get_current_user),
template_repository: SQLAlchemyTemplateRepository = Depends(_get_template_repository),
) -> ListTemplatesResponse:
@@ -116,6 +120,7 @@ def list_templates(
tag=tag,
keyword=keyword,
mode=mode,
valid_only=valid_only,
)
use_case = ListTemplatesUseCase(template_repository)
templates = use_case.execute(user_id, skip=skip, limit=limit, filter=tpl_filter)
@@ -36,17 +36,11 @@ from app.services.asset_segment_tracker import (
remove_used_segment,
)
from app.services.edit_plan_service import EditPlanService
from app.services.edit_template_service import EditTemplateService
from app.services.edit_template_service import EditTemplateService, TemplateNotFoundError
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
from packages.adapters.sqlalchemy_impl.template_clip_config_repository import (
SQLAlchemyTemplateClipConfigRepository,
)
from packages.adapters.sqlalchemy_impl.template_repository import (
SQLAlchemyTemplateRepository,
)
from packages.domain.plan_generator_utils import (
_calc_random_start_time,
build_scene_segments,
@@ -399,68 +393,42 @@ def _safe_segment_duration(value, default: float) -> float:
def _get_template_segments(
template_id: str,
user_id: str,
tpl_svc: EditTemplateService,
db: Session,
) -> list[tuple[int, float, float]]:
"""获取模板的片段配置(顺序、最短时长、最长时长).
优先从新模板系统(template_clip_configs)查询,
若不存在则回退到旧模板系统(template_segments)。
单一数据源:模板主表为 ``templates``(用户自建,归属 user_id)/
``edit_templates``(全局模板库),片段配置主表为 ``template_clip_configs``
(由 ``EditTemplateService.list_clip_configs_for_editor`` 统一读取)。
不再使用"新表抛异常 → 降级直查配置表 → 再降级查 segments"的异常控制流,
也不在正常请求中打印 ``ValueError: 模板不存在`` 堆栈。
Args:
template_id: 模板 ID
user_id: 当前登录用户 ID(用于归属校验)
tpl_svc: 模板编辑器服务
Returns:
[(segment_order, duration_min, duration_max), ...] 按 order 排序
[(segment_order, duration_min, duration_max), ...] 按 order 排序;
模板存在但未配置片段时返回空列表。
Raises:
TemplateNotFoundError: 模板不存在、已删除或不归属于当前用户。
"""
# 优先查新模板系统
try:
clip_configs = tpl_svc.list_clip_configs(template_id)
if clip_configs:
result = []
for cc in clip_configs:
dur_min = _safe_segment_duration(cc.min_duration, _DEFAULT_EDITOR_CLIP_DURATION)
dur_max = _safe_segment_duration(
cc.max_duration or cc.min_duration,
_DEFAULT_EDITOR_CLIP_DURATION,
)
dur_min, dur_max = min(dur_min, dur_max), max(dur_min, dur_max)
result.append((cc.order, dur_min, dur_max))
return sorted(result, key=lambda x: x[0])
except Exception:
logger.warning("新模板系统查询clip_configs失败(主表可能不存在),直接查clip_configs表", exc_info=True)
clip_configs = tpl_svc.list_clip_configs_for_editor(template_id, user_id)
# 兜底:直接查 template_clip_configs 表(片段表有 template_id 外键,不依赖模板主表)
try:
direct_repo = SQLAlchemyTemplateClipConfigRepository(db)
direct_configs = direct_repo.list_by_template(template_id)
if direct_configs:
result = []
for cc in direct_configs:
dur_min = _safe_segment_duration(cc.min_duration, _DEFAULT_EDITOR_CLIP_DURATION)
dur_max = _safe_segment_duration(
cc.max_duration or cc.min_duration,
_DEFAULT_EDITOR_CLIP_DURATION,
)
dur_min, dur_max = min(dur_min, dur_max), max(dur_min, dur_max)
result.append((cc.order, dur_min, dur_max))
return sorted(result, key=lambda x: x[0])
except Exception:
logger.warning("直接查clip_configs表也失败,继续回退旧系统", exc_info=True)
# 回退到旧模板系统(template_segments表)
try:
old_repo = SQLAlchemyTemplateRepository(db)
segments = old_repo.list_segments(template_id)
if segments:
result = []
for s in segments:
dur_min = _safe_segment_duration(s.duration_min, _DEFAULT_EDITOR_CLIP_DURATION)
dur_max = _safe_segment_duration(s.duration_max, _DEFAULT_EDITOR_CLIP_DURATION)
dur_min, dur_max = min(dur_min, dur_max), max(dur_min, dur_max)
result.append((s.segment_order, dur_min, dur_max))
return sorted(result, key=lambda x: x[0])
except Exception:
logger.warning("旧模板系统查询segments失败", exc_info=True)
return []
result = []
for cc in clip_configs:
dur_min = _safe_segment_duration(cc.min_duration, _DEFAULT_EDITOR_CLIP_DURATION)
dur_max = _safe_segment_duration(
cc.max_duration or cc.min_duration,
_DEFAULT_EDITOR_CLIP_DURATION,
)
dur_min, dur_max = min(dur_min, dur_max), max(dur_min, dur_max)
result.append((cc.order, dur_min, dur_max))
return sorted(result, key=lambda x: x[0])
def _recommended_time_conflicts(
@@ -663,13 +631,21 @@ def create_clips_from_assets_editor(
7. 素材时长为 0 或缺失时报 400,不创建无效片段
"""
tpl_svc, plan_svc = services
user_id = str(current_user.user.id)
# 1. 查询模板 segments
segments = _get_template_segments(template_id, tpl_svc, db)
# 1. 查询模板片段配置。模板不存在/已删除/无权限 → 404;
# 模板存在但确实未配置片段 → 422(配置错误,与 404 区分)。
try:
segments = _get_template_segments(template_id, user_id, tpl_svc)
except TemplateNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="模板不存在或无权访问",
) from exc
if not segments:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="模板没有片段配置,无法创建片段",
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="模板未配置片段",
)
# 防御:schema validator 已过滤 null/空串,这里再归一化一次,
@@ -41,29 +41,33 @@ def get_draft_plan_id(
这是模板编辑器路由的核心依赖——所有编辑器端点都先经过这里,
确保 template_id → plan_id 的映射始终存在。
兼容策略:优先从新模板系统(edit_templates 表)查找,
若不存在则回退到旧模板系统(templates 表),确保用户自建模板可用。
模板读取遵循单一数据源、显式判定(不使用异常降级):
- 用户自建模板在旧表 ``templates``(归属 user_id,is_active=True);
- 全局模板在新表 ``edit_templates``(无 user_id,全局可读)。
模板不存在、已删除或不归属于当前用户时,一律返回 404。
"""
tpl_svc, plan_svc = services
user_id = str(current_user.user.id)
# 0. 门禁:校验模板存在且可访问(即使草稿已缓存命中也要校验,
# 避免模板被删除/无权访问后仍可通过既有草稿 plan 继续操作)。
old_repo = SQLAlchemyTemplateRepository(db)
old_template = old_repo.get_active(template_id, user_id)
is_global_template = tpl_svc.get_template(template_id) is not None
if old_template is None and not is_global_template:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
# 1. 草稿已存在 → 直接返回
draft = tpl_svc.get_template_draft(template_id)
if draft is not None:
return draft.id
# 2. 新系统有模板 → 用新服务创建草稿
if tpl_svc.get_template(template_id) is not None:
# 2. 全局模板(新系统)→ 用新服务创建草稿
if is_global_template:
draft = tpl_svc.create_template_draft(template_id, user_id=user_id)
return draft.id
# 3. 回退到旧模板系统(templates 表)
old_repo = SQLAlchemyTemplateRepository(db)
old_template = old_repo.get(template_id, user_id=user_id)
if old_template is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模板不存在")
# 4. 基于旧模板创建草稿计划
# 3. 旧模板(templates 表)→ 基于旧模板创建草稿计划
from app.services.plan_generator_service import PlanGeneratorService
from packages.domain.edit_template import EditTemplate, EditTemplateStatus
@@ -150,7 +150,7 @@ def rollback_template(
try:
tpl = tpl_svc.rollback_to_version(template_id, request.version)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
clip_configs = tpl_svc.list_clip_configs(template_id)
return EditorRollbackResponse(
+5
View File
@@ -208,6 +208,8 @@ def _create_pending_asset(
find-or-create:prepare 阶段已按 file_hash/client_upload_id 预建的占位记录
会被 find_by_library_and_file_hash/find_by_library_and_client_upload_id 命中,
直接复用并补齐字段(避免 pre-create + complete 重复建两条)。
Issue #1776: 素材库计数由 asset_repository.create() 自动维护。
"""
# 1. 按 client_upload_id / file_hash 查找现有记录
existing = None
@@ -389,6 +391,7 @@ async def prepare_direct_upload(
file_size=request.file_size,
)
pending_asset_id = pending.id
# Issue #1776: 计数由 asset_repository.create() 自动维护
except Exception as error:
# 预建失败不阻塞签名:complete 仍可按 OSS 文件 + hash 兜底去重
logger.warning("预建 asset 占位失败,降级走 old flow: %s", error)
@@ -474,6 +477,7 @@ async def complete_direct_upload(
client_upload_id=request.client_upload_id,
file_size=request.file_size,
)
# Issue #1776: 计数由 asset_repository.create() 自动维护
job = _submit_ingest_job(
project_id=request.project_id,
@@ -568,6 +572,7 @@ async def upload_asset(
file_hash=file_hash,
client_upload_id=client_upload_id,
)
# Issue #1776: 计数由 asset_repository.create() 自动维护
job = _submit_ingest_job(
project_id=project_id,
+44 -10
View File
@@ -36,9 +36,13 @@ class CreateGenerationTaskRequest(BaseModel):
template_id: str = ""
asset_ids: list[str] = Field(default_factory=list)
title_ids: list[str] = Field(default_factory=list)
voice_ids: list[str] = Field(default_factory=list)
# ── 来源剪辑计划 ──
source_edit_plan_id: str = ""
# ── variant-plans 轻量选片回传(#1749):正式生成直接复用,不再重选 ──
variant_plan_ids: list[str] = Field(
default_factory=list,
description="POST /generation/variant-plans 返回的各变体 plan_id(长度须=count);为空则走服务端选片",
)
# ── 标题配置(结构化)──
title_config: dict | None = Field(
default=None,
@@ -96,11 +100,31 @@ class CreateGenerationTaskRequest(BaseModel):
@model_validator(mode="after")
def _check_variant_arrays(self) -> "CreateGenerationTaskRequest":
"""变体数组字段长度校验:空数组(回退单值)、长度 1(共用)、或长度 = count(独立)。"""
for name in ("voice_library_ids", "cover_urls", "titles"):
"""变体数组字段长度校验 + #1749 配音严格守卫。
- cover_urls/titles:空(回退单值)、长度 1(共用)或长度 = count(独立);
- voice_library_ids:独立配音长度必须恰好 = count 且逐项非空,禁止静默 fallback
(长度 1 的"共用"场景请用 voice_library_id 单值字段);
- variant_plan_ids:非空时长度必须 = count。
"""
for name in ("cover_urls", "titles"):
arr = getattr(self, name)
if arr and len(arr) != 1 and len(arr) != self.count:
raise ValueError(f"{name} 长度必须为 1(共用)或 {self.count}(与 count 一致),当前为 {len(arr)}")
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
try:
resolve_variant_voice_ids(
count=self.count,
voice_library_id=self.voice_library_id,
voice_library_ids=self.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise ValueError(str(exc)) from exc
if self.variant_plan_ids and len(self.variant_plan_ids) != self.count:
raise ValueError(f"variant_plan_ids 长度({len(self.variant_plan_ids)})必须与 count({self.count})一致")
return self
@model_validator(mode="after")
@@ -110,9 +134,9 @@ class CreateGenerationTaskRequest(BaseModel):
if not has_project and not has_template:
raise ValueError("project_id 或 template_id 至少需要提供一个")
has_library = bool(self.asset_library_id.strip())
has_assets = bool(self.asset_ids or self.title_ids or self.voice_ids)
has_assets = bool(self.asset_ids or self.title_ids)
if not has_library and not has_assets:
raise ValueError("asset_library_id 或 asset_ids/title_ids/voice_ids 至少需要提供一个")
raise ValueError("asset_library_id 或 asset_ids/title_ids 至少需要提供一个")
return self
@@ -189,7 +213,6 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
template_id: str
asset_ids: list[str] = Field(default_factory=list)
title_ids: list[str] = Field(default_factory=list)
voice_ids: list[str] = Field(default_factory=list)
voice_library_id: str = Field(
default="", description="配音素材库ID(用户上传的音频或AI配音),对应配音选择页面选择的配音素材"
)
@@ -231,13 +254,24 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
@model_validator(mode="after")
def _check_variant_arrays(self) -> "CreatePreviewGenerationTaskRequest":
"""变体数组字段长度校验:空数组(回退单值)、长度 1(共用)、或长度 = preview_count(独立)。"""
for name in ("titles", "voice_library_ids", "cover_urls"):
"""变体数组字段长度校验 + #1749 配音严格守卫。"""
for name in ("titles", "cover_urls"):
arr = getattr(self, name)
if arr and len(arr) != 1 and len(arr) != self.preview_count:
raise ValueError(
f"{name} 长度必须为 1(共用)或 {self.preview_count}(与 preview_count 一致),当前为 {len(arr)}"
)
from packages.domain.variant_voice_resolver import VariantVoiceError, resolve_variant_voice_ids
try:
resolve_variant_voice_ids(
count=self.preview_count,
voice_library_id=self.voice_library_id,
voice_library_ids=self.voice_library_ids or None,
)
except VariantVoiceError as exc:
raise ValueError(str(exc)) from exc
return self
@model_validator(mode="after")
@@ -248,8 +282,8 @@ class CreatePreviewGenerationTaskRequest(BaseModel):
@model_validator(mode="after")
def _check_asset_ids(self) -> "CreatePreviewGenerationTaskRequest":
if not self.asset_ids and not self.title_ids and not self.voice_ids:
raise ValueError("asset_ids/title_ids/voice_ids 至少需要提供一个")
if not self.asset_ids and not self.title_ids:
raise ValueError("asset_ids/title_ids 至少需要提供一个")
return self
+45
View File
@@ -0,0 +1,45 @@
"""Script (口播文案库) Pydantic schemas — Issue #1795."""
from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
class ScriptSegment(BaseModel):
"""单段文案."""
text: str
duration: Optional[float] = None
class ScriptResponse(BaseModel):
id: str
user_id: str
title: str
content: str
segments: List[ScriptSegment] = Field(default_factory=list)
tags: List[str] = Field(default_factory=list)
created_at: datetime
updated_at: datetime
class ScriptListResponse(BaseModel):
items: list[ScriptResponse]
total: int = 0
class CreateScriptRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
content: str = ""
segments: List[ScriptSegment] = Field(default_factory=list)
tags: List[str] = Field(default_factory=list)
class UpdateScriptRequest(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=255)
content: Optional[str] = None
segments: Optional[List[ScriptSegment]] = None
tags: Optional[List[str]] = None
+423 -3
View File
@@ -409,8 +409,13 @@ class EditPlanService:
clip_type=clip_item.get("clip_type", "main"),
order=order,
asset_id=clip_item.get("asset_id", ""),
text_content=clip_item.get("text_content", ""),
start_time=clip_item.get("start_time", 0.0),
duration=clip_item.get("duration", 0.0),
transition_effect=clip_item.get("transition_effect", "cut"),
transition_duration=clip_item.get("transition_duration", 0.0),
playback_speed=clip_item.get("playback_speed", 1.0),
config=clip_item.get("config") or None,
)
model = EditPlanClipModel(
id=clip.id,
@@ -459,6 +464,159 @@ class EditPlanService:
logger.exception("事务性替换片段失败: plan_id=%s", plan_id)
raise
def reselect_plan_for_variant(
self,
source_plan_id: str,
candidate_asset_ids: list[str],
*,
created_by_user_id: str = "",
name_suffix: str = "变体",
voice_duration: float = 0.0,
rng=None,
) -> EditPlan:
"""为批量变体生成独立 plan:完整重跑单视频选片流程(#1743)。
与 clone_plan_for_variant(只重算起点、素材/顺序不变)不同,本方法:
- 源 plan 片段骨架(clip_type/order/duration/文案/转场)保留;
- 素材池 shuffle 随机分配 + main 片段顺序洗牌;
- 起点走场景镜头洗牌/随机起点/历史区间避让(与单视频同一入口);
- 批次内同素材区间重叠 >20% 自动重选起点;
- 新片段区间 record_used_segments 写回素材 metadata(跨变体/跨任务避让)。
Args:
source_plan_id: 源 plan(任务 0 / 预览源)。
candidate_asset_ids: 素材池(源 plan 素材 ∪ 批次素材)。
created_by_user_id: 新 plan 归属用户。
name_suffix: plan 名后缀。
rng: 可选随机数(测试注入种子)。
Raises:
ValueError: 源 plan 不存在/无片段、素材池为空或时长全未知。
"""
from packages.adapters.sqlalchemy_impl.models import AssetModel
from packages.domain.plan_generator_utils import extract_scene_points_from_metadata
from packages.domain.variant_plan_selector import reselect_clips_for_variant
source = self.get_plan_or_raise(source_plan_id)
# 分页读取源 plan 全部片段
clips: List[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(source_plan_id, skip=skip, limit=page)
if not batch:
break
clips.extend(batch)
if len(batch) < page:
break
skip += page
if not clips:
raise ValueError(f"源 plan 无片段,无法生成变体: {source_plan_id}")
source_clips_data: list[dict[str, Any]] = [
{
"order": c.order if c.order is not None else i,
"asset_id": c.asset_id,
"start_time": float(c.start_time or 0.0),
"duration": float(c.duration or 0.0),
"clip_type": c.clip_type,
"playback_speed": float(c.playback_speed or 1.0),
"transition_effect": c.transition_effect,
"transition_duration": float(c.transition_duration or 0.0),
"text_content": c.text_content or "",
"config": c.config or {},
}
for i, c in enumerate(clips)
]
db = self._clip_repo.session
# #1749:配音时长 → 每段目标段长(片段数=模板片段数定死;素材不足由渲染末帧冻结铺满)
target_durations: list[float] | None = None
try:
voice = float(voice_duration or 0.0)
except (TypeError, ValueError):
voice = 0.0
if voice > 0 and source_clips_data:
from packages.domain.voice_duration_planner import plan_clip_durations
_effects: list[str | None] = [c.get("transition_effect") for c in source_clips_data]
_tdurs: list[float] = [float(c.get("transition_duration") or 0.0) for c in source_clips_data]
target_durations = plan_clip_durations(
len(source_clips_data),
voice,
transition_effects=_effects,
transition_durations=_tdurs,
)
if target_durations:
for _c, _d in zip(source_clips_data, target_durations, strict=False):
_c["duration"] = _d
# 素材池 = 源 plan 素材 ∪ 调用方传入素材(去重保序)
pool_ids: list[str] = []
seen = set()
for aid in [c.asset_id for c in clips if c.asset_id] + list(candidate_asset_ids or []):
if aid and aid not in seen:
seen.add(aid)
pool_ids.append(aid)
# 时长 + 场景点
durations: dict[str, float] = {}
scene_points: dict[str, list[float]] = {}
if pool_ids:
for m in db.query(AssetModel).filter(AssetModel.id.in_(pool_ids)).all():
durations[m.id] = float(getattr(m, "duration", 0.0) or 0.0)
pts = extract_scene_points_from_metadata(getattr(m, "metadata", None))
if pts:
scene_points[m.id] = pts
historical = get_used_segments(db, pool_ids)
# 创建新 plan(复制模板归属与 config)
new_plan = self.create_plan(
template_id=source.template_id,
name=f"{source.name or '剪辑计划'} · {name_suffix}",
config=dict(source.config or {}),
total_duration=source.total_duration,
project_id=source.project_id or "",
created_by_user_id=created_by_user_id or (source.created_by_user_id or ""),
)
# 批次内区间:以源 plan(变体 0)片段为初始避让对象
batch_segments: dict[str, list[tuple[float, float]]] = {}
for c in clips:
if c.asset_id and float(c.duration or 0) > 0:
st = float(c.start_time or 0.0)
batch_segments.setdefault(c.asset_id, []).append((st, st + float(c.duration)))
clips_data = reselect_clips_for_variant(
source_clips_data,
pool_ids,
asset_durations=durations,
asset_scene_points=scene_points,
historical_used_segments=historical,
batch_segments=batch_segments,
target_durations=target_durations,
rng=rng,
)
# 片段区间写回素材 metadata(与落库同事务;replace_all_clips_transactional 内 commit)
for item in clips_data:
aid = item.get("asset_id", "")
if aid:
st = float(item.get("start_time", 0.0))
record_used_segments(db, aid, st, st + float(item.get("duration", 0.0)), new_plan.id)
self.replace_all_clips_transactional(new_plan.id, clips_data)
logger.info(
"变体独立选片完成: source=%s new=%s clips=%d assets=%d",
source_plan_id,
new_plan.id,
len(clips_data),
len(pool_ids),
)
return new_plan
def clone_plan_for_variant(
self,
source_plan_id: str,
@@ -569,6 +727,268 @@ class EditPlanService:
)
return new_plan
# ── #1749 配音时长分配 / 素材时长查询 / 批量变体 plan 确保 ──────────────
def get_asset_durations(self, asset_ids: list[str]) -> dict[str, float]:
"""批量查询素材时长(秒),O(N) 单查;缺失/异常返回 0.0。"""
from packages.adapters.sqlalchemy_impl.models import AssetModel
ids = [a for a in dict.fromkeys(asset_ids or []) if a]
if not ids:
return {}
db = self._clip_repo.session
out: dict[str, float] = {}
for m in db.query(AssetModel).filter(AssetModel.id.in_(ids)).all():
try:
out[m.id] = float(getattr(m, "duration", 0.0) or 0.0)
except (TypeError, ValueError):
out[m.id] = 0.0
return out
def apply_voice_duration_to_plan(self, plan_id: str, voice_duration: float) -> Optional[EditPlan]:
"""把配音时长分配到 plan 的每段(#1749)。
- 片段数保持不变(= 模板片段数,定死);
- 每段 duration 按 voice_duration_planner 分配(含转场重叠扣减);
- 素材短于段长 → start_time 钳制为 0(末帧冻结由渲染侧 tpad/apad 铺满);
- plan.total_duration 回写为成片净时长(≈ 配音时长);
- 幂等:配音时长相同则分配结果不变,可重复调用。
无配音(<=0)或无片段时直接返回 None,不报错。
"""
try:
voice = float(voice_duration or 0.0)
except (TypeError, ValueError):
return None
if voice <= 0:
return None
plan = self.get_plan(plan_id)
if plan is None:
return None
clips: List[EditPlanClip] = []
skip, page = 0, 500
while True:
batch = self._clip_repo.list_by_plan(plan_id, skip=skip, limit=page)
if not batch:
break
clips.extend(batch)
if len(batch) < page:
break
skip += page
if not clips:
return None
clips.sort(key=lambda c: (c.order if c.order is not None else 0))
from packages.domain.voice_duration_planner import plan_clip_durations, total_output_duration
# #1764:从 plan config 读取节奏模板
rhythm_template = None
if plan and hasattr(plan, "config") and plan.config:
rhythm_template = plan.config.get("rhythm_template")
# #1768:先获取素材时长,传入 plan_clip_durations 用于最大片段钳制
asset_ids = [c.asset_id for c in clips if c.asset_id]
durations = self.get_asset_durations(asset_ids)
asset_durations_for_plan = [durations.get(c.asset_id, 0.0) for c in clips]
target = plan_clip_durations(
len(clips),
voice,
transition_effects=[c.transition_effect for c in clips],
transition_durations=[float(c.transition_duration or 0.0) for c in clips],
rhythm_template=rhythm_template,
asset_durations=asset_durations_for_plan,
)
if not target:
return None
clips_data: list[dict] = []
for i, c in enumerate(clips):
dur = float(target[i])
total = durations.get(c.asset_id, 0.0)
start = float(c.start_time or 0.0)
if c.asset_id and total > 0:
# 素材短于段长:起点钳 0,段长超出部分渲染侧末帧冻结
max_start = max(0.0, total - min(dur, total))
start = min(start, max_start)
clips_data.append(
{
"order": c.order if c.order is not None else i,
"asset_id": c.asset_id or "",
"start_time": round(start, 3),
"duration": dur,
"clip_type": c.clip_type,
"playback_speed": float(c.playback_speed or 1.0),
"transition_effect": c.transition_effect,
"transition_duration": float(c.transition_duration or 0.0),
"text_content": c.text_content or "",
"config": c.config or {},
}
)
self.replace_all_clips_transactional(plan_id, clips_data)
net = total_output_duration(
target,
transition_effects=[c.transition_effect for c in clips],
transition_durations=[float(c.transition_duration or 0.0) for c in clips],
)
try:
plan.total_duration = net
db = self._clip_repo.session
db.commit()
except Exception:
db.rollback()
logger.exception("回写 plan.total_duration 失败(不阻断): plan_id=%s", plan_id)
logger.info(
"配音时长分配完成: plan=%s clips=%d voice=%.2fs 成片净时长=%.2fs",
plan_id,
len(clips),
voice,
net,
)
return plan
def ensure_variant_plans(
self,
source_plan_id: str,
count: int,
candidate_asset_ids: list[str],
*,
created_by_user_id: str = "",
voice_durations: Optional[list[float]] = None,
rng=None,
) -> list[str]:
"""确保批量 N 个变体各自拥有独立 plan(#1749 批量正式生成/预览共用)。
- 变体 0:clone 源 plan(不污染源 plan,片段独立可改),并按配音分配段长;
- 变体 1..N-1:reselect_plan_for_variant 完整重跑选片(素材级去重);
- voice_durations:每个变体的配音时长(独立配音各自时长;统一配音同值);
缺省/为 0 时不分配(段长保持骨架/模板值)。
Returns:
plan_id 列表,长度 == count,index 即 variant_index。
"""
import random as _random
rng = rng or _random.Random()
plan_ids: list[str] = []
# 变体 0:clone(片段结构同源 plan,起点重算),不污染源 plan
plan0 = self.clone_plan_for_variant(
source_plan_id,
created_by_user_id=created_by_user_id,
name_suffix="变体1",
)
v0_voice = 0.0
if voice_durations and len(voice_durations) > 0:
try:
v0_voice = float(voice_durations[0] or 0.0)
except (TypeError, ValueError):
v0_voice = 0.0
if v0_voice > 0:
try:
self.apply_voice_duration_to_plan(plan0.id, v0_voice)
except Exception:
logger.exception("变体0 配音分配失败(不阻断): plan=%s", plan0.id)
plan_ids.append(plan0.id)
# 变体 1..N-1:独立选片
for i in range(1, count):
voice = 0.0
if voice_durations and i < len(voice_durations):
try:
voice = float(voice_durations[i] or 0.0)
except (TypeError, ValueError):
voice = 0.0
variant = self.reselect_plan_for_variant(
source_plan_id,
candidate_asset_ids,
created_by_user_id=created_by_user_id,
name_suffix=f"变体{i + 1}",
voice_duration=voice,
rng=rng,
)
plan_ids.append(variant.id)
# #1764:为每个变体生成独立节奏模板(让批量视频片段时长分布不同)
from packages.domain.voice_duration_planner import RHYTHM_TEMPLATES, adapt_template_length
clip_count = 0
if voice_durations and len(voice_durations) > 0:
# 从源 plan 获取片段数
source_plan = self.get_plan(source_plan_id)
if source_plan and hasattr(source_plan, "clips"):
clip_count = len(list(source_plan.clips)) if source_plan.clips else 0
rhythm_templates_for_variants = []
if clip_count > 0:
for idx in range(len(plan_ids)):
# 每个变体用不同的 seed 选择节奏模板
variant_seed = rng.randint(0, 999999)
template = adapt_template_length(RHYTHM_TEMPLATES[variant_seed % len(RHYTHM_TEMPLATES)], clip_count)
rhythm_templates_for_variants.append(template)
logger.info("变体 %d 节奏模板: plan=%s template=%s", idx, plan_ids[idx], template)
# #1767:BGM 池差异化分配(让批量变体使用不同 BGM / 段落 / 音量)
from packages.domain.bgm_pool import allocate_bgm_pool_for_variants
source_bgm_config = {}
source_plan = self.get_plan(source_plan_id)
if source_plan and source_plan.config:
source_bgm_config = source_plan.config.get("bgm", {}) or {}
variant_seeds_for_bgm = [rng.randint(0, 999999) for _ in plan_ids]
bgm_pool_assignments = allocate_bgm_pool_for_variants(source_bgm_config, variant_seeds_for_bgm)
# 为每个变体生成独立视觉扰动参数(让批量视频画面本身更不同)
from packages.domain.variant_plan_selector import generate_visual_perturbation
for idx, pid in enumerate(plan_ids):
try:
perturbation = generate_visual_perturbation(rng)
# 变体 0 不做 hflip(保持预览 plan 原始画面方向)
if idx == 0:
perturbation["hflip"] = False
config_update = {"visual_perturbation": perturbation}
# #1764:写入节奏模板
if idx < len(rhythm_templates_for_variants):
config_update["rhythm_template"] = rhythm_templates_for_variants[idx]
# #1765:写入像素级扰动滤镜
from packages.domain.variant_plan_selector import generate_pixel_perturbation
pixel_pert = generate_pixel_perturbation(rng)
config_update["pixel_perturbation"] = pixel_pert
# #1767:写入 BGM 池分配(覆盖 bgm 配置中的 preset_id / audio_offset / volume_adjust_db)
if idx < len(bgm_pool_assignments):
existing_bgm = dict((source_plan.config or {}).get("bgm", {}) or {})
existing_bgm.update(bgm_pool_assignments[idx])
config_update["bgm"] = existing_bgm
self.update_plan_config(pid, config_update)
logger.info(
"变体 %d 视觉扰动+像素扰动+BGM池: plan=%s vis=%s pix=%s bgm=%s",
idx,
pid,
perturbation,
pixel_pert,
bgm_pool_assignments[idx] if idx < len(bgm_pool_assignments) else None,
)
except Exception:
logger.exception("变体 %d 视觉扰动生成失败(不阻断): plan=%s", idx, pid)
# 标记所有变体 plan 的 clips 为 ready(已分配素材+起点,语义上就是 ready)
for pid in plan_ids:
try:
self.mark_clips_ready(pid)
except Exception:
logger.exception("标记 clips ready 失败(不阻断): plan=%s", pid)
return plan_ids
# ── 片段分割与合并 ──────────────────────────────────────────────────────
def split_clip(self, clip_id: str, split_time: float) -> Dict[str, Any]:
@@ -792,7 +1212,7 @@ class EditPlanService:
config_asset_ids_count = len((plan.config or {}).get("asset_ids", []))
clips_with_asset_count = sum(1 for c in clips if c.asset_id)
logger.info(
"can_generate 诊断: plan=%s status=%s total_clips=%d " "clips_with_asset=%d config_asset_ids_count=%d",
"can_generate 诊断: plan=%s status=%s total_clips=%d clips_with_asset=%d config_asset_ids_count=%d",
plan_id,
plan.status,
len(clips),
@@ -804,7 +1224,7 @@ class EditPlanService:
config_asset_ids = (plan.config or {}).get("asset_ids", [])
if config_asset_ids:
logger.warning(
"can_generate 最后防线触发: plan=%s clips=%d 均无素材," "从 config.asset_ids(%d个) 自动分配",
"can_generate 最后防线触发: plan=%s clips=%d 均无素材,从 config.asset_ids(%d个) 自动分配",
plan_id,
len(clips),
len(config_asset_ids),
@@ -836,7 +1256,7 @@ class EditPlanService:
return False, "没有可渲染的就绪片段,自动修复后仍未分配素材"
else:
logger.warning(
"can_generate 失败: plan=%s clips=%d 均无素材," "且 config.asset_ids 为空,无法自动修复",
"can_generate 失败: plan=%s clips=%d 均无素材,且 config.asset_ids 为空,无法自动修复",
plan_id,
len(clips),
)
+65 -1
View File
@@ -34,6 +34,17 @@ from packages.domain.template_clip_converter import (
logger = logging.getLogger(__name__)
class TemplateNotFoundError(Exception):
"""模板不存在、已删除或当前用户无权访问.
与"模板存在但无片段配置"区分:路由层应映射为 HTTP 404。
"""
def __init__(self, template_id: str) -> None:
self.template_id = template_id
super().__init__(f"模板不存在: {template_id}")
class EditTemplateService:
"""模板管理服务
@@ -217,7 +228,14 @@ class EditTemplateService:
skip: int = 0,
limit: int = 100,
) -> List[TemplateClipConfig]:
"""列出模板的片段配置"""
"""列出模板的片段配置
注意:本方法要求模板存在于新表 ``edit_templates``(全局模板库),
主要服务于新模板系统的写入/发布路径。用户自建模板存放在旧表
``templates``,不在 ``edit_templates`` 中,读取其片段配置请改用
:meth:`list_clip_configs_for_editor`,后者直接读取片段配置主表
``template_clip_configs``,不依赖新模板主表、也不靠异常降级。
"""
# 确保模板存在
self.get_template_or_raise(template_id)
return self._clip_config_repo.list_by_template(
@@ -227,6 +245,52 @@ class EditTemplateService:
limit=limit,
)
def list_clip_configs_for_editor(
self,
template_id: str,
user_id: str,
*,
clip_type: Optional[ClipType] = None,
skip: int = 0,
limit: int = 100,
) -> List[TemplateClipConfig]:
"""编辑器读取模板片段配置的单一数据源入口.
片段配置主表是 ``template_clip_configs``(直接读取,不抛异常、不降级)。
模板主表按双表现状显式判定,不使用 try/except 控制流:
1. 用户自建模板在旧表 ``templates``(归属 user_id)→ 校验归属与未删除后直接读;
2. 全局模板在新表 ``edit_templates``(无 user_id,全局可读)→ 直接读;
3. 两者都没有 → 模板不存在/无权限,抛 :class:`TemplateNotFoundError`。
Args:
template_id: 模板 ID
user_id: 当前登录用户 ID(用于旧表模板归属校验)
Raises:
TemplateNotFoundError: 模板不存在、已删除或不归属于当前用户。
"""
# 1) 用户自建模板(旧表 templates,归属 user_id)
if self._clip_config_repo.template_owned_by(template_id, user_id):
return self._clip_config_repo.list_by_template(
template_id,
clip_type=clip_type,
skip=skip,
limit=limit,
)
# 2) 全局模板(新表 edit_templates,无 user_id,全局可读)
if self._template_repo.get(template_id) is not None:
return self._clip_config_repo.list_by_template(
template_id,
clip_type=clip_type,
skip=skip,
limit=limit,
)
# 3) 两表都没有:不存在 / 已删除 / 无权限
raise TemplateNotFoundError(template_id)
def get_clip_config(self, config_id: str) -> Optional[TemplateClipConfig]:
"""获取片段配置详情"""
return self._clip_config_repo.get(config_id)
+109
View File
@@ -0,0 +1,109 @@
"""ScriptService — Issue #1795 口播文案库 CRUD.
纯 Service 层封装,routes 直接调用。
"""
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import ScriptModel
class ScriptNotFoundError(Exception):
"""文案不存在或不属于当前用户."""
class ScriptService:
"""口播文案 CRUD."""
def __init__(self, db: Session) -> None:
self.db = db
# ── list ──────────────────────────────────────────────────────────────
def list_scripts(
self,
user_id: str,
skip: int = 0,
limit: int = 50,
tag: Optional[str] = None,
) -> tuple[list[ScriptModel], int]:
"""返回 (items, total)."""
q = self.db.query(ScriptModel).filter(ScriptModel.user_id == user_id)
if tag:
# JSON 数组包含查询
q = q.filter(ScriptModel.tags.contains([tag]))
total = q.count()
items = q.order_by(ScriptModel.created_at.desc()).offset(skip).limit(limit).all()
return items, total
# ── create ────────────────────────────────────────────────────────────
def create_script(
self,
user_id: str,
title: str,
content: str = "",
segments: list | None = None,
tags: list | None = None,
) -> ScriptModel:
script = ScriptModel(
id=str(uuid.uuid4()),
user_id=user_id,
title=title,
content=content,
segments=segments if segments is not None else [],
tags=tags if tags is not None else [],
)
self.db.add(script)
self.db.commit()
self.db.refresh(script)
return script
# ── get ───────────────────────────────────────────────────────────────
def get_script(self, script_id: str, user_id: str) -> ScriptModel:
script = self.db.query(ScriptModel).filter(ScriptModel.id == script_id, ScriptModel.user_id == user_id).first()
if script is None:
raise ScriptNotFoundError(f"Script {script_id} not found")
return script
# ── update ────────────────────────────────────────────────────────────
def update_script(
self,
script_id: str,
user_id: str,
title: Optional[str] = None,
content: Optional[str] = None,
segments: Optional[list] = None,
tags: Optional[list] = None,
) -> ScriptModel:
script = self.get_script(script_id, user_id)
if title is not None:
script.title = title
if content is not None:
script.content = content
if segments is not None:
script.segments = segments
if tags is not None:
script.tags = tags
script.updated_at = datetime.now(timezone.utc)
self.db.commit()
self.db.refresh(script)
return script
# ── delete ────────────────────────────────────────────────────────────
def delete_script(self, script_id: str, user_id: str) -> bool:
script = self.db.query(ScriptModel).filter(ScriptModel.id == script_id, ScriptModel.user_id == user_id).first()
if script is None:
return False
self.db.delete(script)
self.db.commit()
return True
@@ -39,6 +39,9 @@ from packages.domain.video_filter_builder import (
)
from packages.domain.video_filter_builder import build_concat_filter as _build_concat_filter_func
from packages.domain.video_filter_builder import build_filter_complex as _build_filter_complex
from packages.domain.video_filter_builder import (
build_title_drawtext_filter,
)
from packages.domain.video_filter_builder import build_xfade_filter as _build_xfade_filter_func
from packages.domain.video_filter_builder import chain_filters as _chain_filters_func
from packages.domain.video_filter_builder import has_audio as _has_audio_func
@@ -248,6 +251,27 @@ class VideoComposeService:
transitions=[c.transition_effect for c in ready_clips],
)
# ── #1789 标题 drawtext 滤镜叠加 ──
# 从 plan.config 读取 title_config,生成 drawtext 滤镜链入 filter_complex
title_cfg = (plan.config or {}).get("title", {}) or {}
if not isinstance(title_cfg, dict):
title_cfg = {}
# 同时兼容 plan.config["title_config"](API 回写路径)
if not title_cfg.get("text") and not title_cfg.get("content"):
title_cfg_alt = (plan.config or {}).get("title_config", {}) or {}
if isinstance(title_cfg_alt, dict) and (title_cfg_alt.get("text") or title_cfg_alt.get("content")):
title_cfg = title_cfg_alt
drawtext_filter = build_title_drawtext_filter(title_cfg, output_width, output_height)
if drawtext_filter:
# 将最终输出标签从 [outv] 改为 [composed],再链入 drawtext → [outv]
filter_complex = filter_complex.replace("[outv]", "[composed]")
filter_complex += f";[composed]{drawtext_filter}[outv]"
logger.info(
"[#1789] 标题 drawtext 滤镜已注入: plan_id=%s text=%s",
plan_id,
(title_cfg.get("text") or title_cfg.get("content") or "")[:30],
)
# 构建完整命令
command: list[str] = ["ffmpeg", "-y"]
+16 -4
View File
@@ -5,10 +5,22 @@ import apiClient from "../client"
import { getOrCreateDefaultProject } from "../projects"
import type { AssetLibraryItem } from "./types"
/** 获取当前用户的所有素材库 */
export const getAssetLibraries = async (): Promise<AssetLibraryItem[]> => {
const response = await apiClient.get("/asset-libraries")
return response.data.items || []
/**
* 获取当前用户的素材库
*
* @param kind 可选,按素材库类型过滤(video/voice/image)。
* 后端 GET /asset-libraries 支持 kind 查询参数;这里同时在前端再按返回数据的
* kind 字段兜底过滤一次,保证旧后端(忽略未知 query 参数)也不会把其他类型的库
* 混进来(#1777:视频选择器只展示视频库)。
*/
export const getAssetLibraries = async (
kind?: AssetLibraryItem["kind"],
): Promise<AssetLibraryItem[]> => {
const response = await apiClient.get<{ items?: AssetLibraryItem[] }>("/asset-libraries", {
params: kind ? { kind } : undefined,
})
const items = response.data.items || []
return kind ? items.filter((lib) => lib.kind === kind) : items
}
/** 创建素材库(自动获取或创建默认项目以提供 project_id) */
+13
View File
@@ -55,6 +55,19 @@ apiClient.interceptors.response.use(
async (error: AxiosError<{ detail?: string; message?: string; msg?: string }>) => {
const originalRequest = error.config as InternalAxiosRequestConfig & {
_retry?: boolean
/**
* 调用方自行处理错误提示时置 true:拦截器跳过全局 message 弹窗(#1777)。
* 例如失效模板自动回退时,调用方会弹「原模板已失效,已自动切换」,
* 不再叠加后端原始错误文案。错误仍会 reject,不影响 catch 逻辑。
*/
_silentErrorToast?: boolean
}
// 调用方声明自行处理提示:标记为已展示,跳过下面所有全局 message 弹窗
if (originalRequest?._silentErrorToast) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(error as any).__msgShown = true
return Promise.reject(error)
}
// 401 → 尝试刷新 Token
@@ -12,17 +12,25 @@ import type {
ListCategoriesResponse,
} from "./types"
/** 获取模板列表 */
/** 获取模板列表
*
* valid_only=true 时请求后端仅返回已配置片段的模板(剪辑页选模板使用,
* 避免选中无片段配置的模板导致 from-assets 400,#1769/#1772);
* 后端尚未支持该参数时会忽略未知 query 字段,前端再按 segments/is_active 兜底过滤。
* 模板编辑器/我的模板不传,可查看全部模板(含未配置片段的草稿)。
*/
export const getEditingTemplates = async (params?: {
category?: string
tag?: string
skip?: number
limit?: number
validOnly?: boolean
}): Promise<EditingTemplate[]> => {
const response = await apiClient.get<ListTemplatesResponse>("/templates", {
params: {
skip: params?.skip ?? 0,
limit: params?.limit ?? 50,
...(params?.validOnly ? { valid_only: true } : {}),
},
})
let list = response.data.items
+8 -4
View File
@@ -44,8 +44,10 @@ export interface BgmConfig {
export interface TemplateSegment {
id?: string
segment_order: number
duration_min: number
duration_max: number
/** @deprecated 模板无时长概念(#1750 基线):字段保留仅为兼容旧数据读取,新模板可不传 */
duration_min?: number
/** @deprecated 同上 */
duration_max?: number
material_type: string | null
}
@@ -59,7 +61,8 @@ export interface EditingTemplate {
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
/** @deprecated 模板无时长概念(#1750 基线):成片时长由配音时长决定;字段保留兼容旧数据 */
estimated_duration?: number
segments: TemplateSegment[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
@@ -89,7 +92,8 @@ export interface SaveTemplatePayload {
title_config: TitleConfig
subtitle_config: SubtitleConfig
bgm_config: BgmConfig
estimated_duration: number
/** @deprecated 模板无时长概念(#1750 基线):保留兼容旧数据 */
estimated_duration?: number
segments: Omit<TemplateSegment, "id">[]
watermark_config?: WatermarkConfig
intro_outro_config?: IntroOutroConfig
@@ -0,0 +1,61 @@
/**
* 批量变体剪辑计划 API(#1744)
*
* 批量预览时向后端申请 N 个变体的「独立剪辑计划片段」:
* - 变体 0 保留源 plan(用户在编辑器/智能选片产出的片段,含标题样式编辑结果);
* - 变体 1..N-1 由后端 reselect_plan_for_variant 完整重跑单视频选片流程
* (素材洗牌 + main 片段顺序洗牌 + 镜头/起点随机 + 跨变体 20% 区间避让 +
* 素材使用区间写回 metadata),与正式批量生成 POST /generation/tasks?count=N
* 使用同一套选片逻辑;
* - 正式生成时把 variant_plan_ids 原样回传,后端直接关联这些 plan 渲染,
* 不再重新选片 —— 预览所见即成片。
*
* 该接口只做选片/建 plan(秒级),不触发视频渲染,无渲染成本。
* 后端端点未上线(404)或选片失败(素材不足等)时前端降级为本地 variantSeed
* 模拟预览,不阻塞用户流程。
*/
import apiClient from "../client"
import type { EditPlanClip } from "../template-editor"
/** 批量变体计划请求体 */
export interface BatchVariantPlansRequest {
template_id: string
/** 本批次素材池(手动选择或智能匹配结果) */
asset_ids: string[]
/** 变体数量(≥1);=1 时只返回源 plan 片段 */
count: number
/** 源剪辑计划 ID:优先取预览/草稿关联的 plan;不传由后端按 template_id+user 兜底最新 plan */
source_edit_plan_id?: string
}
/** 单个变体的计划片段 */
export interface VariantPlan {
/** 变体序号,从 0 开始 */
variant_index: number
/** 该变体关联的剪辑计划 ID(正式生成时回传,实现预览即成片) */
plan_id: string
/** 该变体的真实片段(顺序/素材/起点与正式成片一致) */
clips: EditPlanClip[]
}
/** 批量变体计划响应 */
export interface BatchVariantPlansResponse {
items: VariantPlan[]
total: number
}
/**
* 创建批量变体剪辑计划并返回各变体片段。
*
* 注意:端点 404(后端未上线)/ 400(素材不足)等失败由调用方 catch 后降级,
* 不要抛 unhandled rejection。
*/
export async function createBatchVariantPlans(
params: BatchVariantPlansRequest,
): Promise<BatchVariantPlansResponse> {
const response = await apiClient.post<BatchVariantPlansResponse>(
"/generation/variant-plans",
params,
)
return response.data
}
+6
View File
@@ -100,6 +100,12 @@ export interface CreateGenerationTaskRequest {
voice_library_ids?: string[]
/** 各变体独立封面URL:长度1=共用,长度=count=独立,空数组=回退 cover_url */
cover_urls?: string[]
/**
* 批量变体剪辑计划 ID(#1744):预览阶段后端独立选片产出的 plan id 列表
* (按变体全量索引,长度=previewCount)。正式生成回传后后端直接关联这些
* plan 渲染、不再重新选片,保证预览所见即成片。后端未支持时忽略该字段。
*/
variant_plan_ids?: string[]
}
/** 单个生成任务详情(对齐后端 GenerationTaskResponse) */
+7 -2
View File
@@ -91,7 +91,7 @@ export async function createClipsFromAssets(
assetIds: string[],
clipType = "main",
requiredClipsCount?: number,
opts?: { signal?: AbortSignal },
opts?: { signal?: AbortSignal; silentErrorToast?: boolean },
): Promise<ClipsFromAssetsResponse> {
const body: Record<string, unknown> = {
asset_ids: assetIds,
@@ -104,7 +104,12 @@ export async function createClipsFromAssets(
const response = await apiClient.post<ClipsFromAssetsResponse>(
`/templates/${templateId}/editor/clips/from-assets`,
body,
{ timeout: 60000, signal: opts?.signal },
{
timeout: 60000,
signal: opts?.signal,
// _silentErrorToast 由 api/client.ts 响应拦截器读取(抑制全局错误 toast,#1777)
...(opts?.silentErrorToast ? ({ _silentErrorToast: true } as Record<string, unknown>) : {}),
},
)
return response.data
}
@@ -43,11 +43,17 @@ export async function updateEditPlanClips(
templateId: string,
clips: EditPlanClipInput[],
signal?: AbortSignal,
/** 为 true 时抑制全局错误 toast(调用方自行提示,如失效模板回退 #1777) */
silentErrorToast?: boolean,
): Promise<{ count: number }> {
const response = await apiClient.put(
`/templates/${templateId}/editor/clips`,
{ clips },
{ signal },
{
signal,
// _silentErrorToast 由 api/client.ts 响应拦截器读取(抑制全局错误 toast)
...(silentErrorToast ? ({ _silentErrorToast: true } as Record<string, unknown>) : {}),
},
)
return response.data
}
+4 -4
View File
@@ -42,8 +42,10 @@ export interface TemplateItem {
export interface TemplateSegment {
id?: string
segment_order: number
duration_min: number
duration_max: number
/** @deprecated 模板无时长概念(#1750 基线):字段保留仅为兼容旧数据读取 */
duration_min?: number
/** @deprecated 同上 */
duration_max?: number
material_type: string | null
description?: string
}
@@ -55,8 +57,6 @@ export interface TemplateListParams {
category?: string
tags?: string
keyword?: string
/** 时长筛选(秒):short < 30, medium 30-120, long > 120 */
duration_range?: "short" | "medium" | "long"
}
/** 模板列表分页响应 */
@@ -6440,3 +6440,47 @@
opacity: 0.6;
cursor: not-allowed;
}
/* ═══ 标题设置 — 颜色预设 ═══ */
.ep-color-presets {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.ep-color-swatch {
width: 24px;
height: 24px;
border-radius: 4px;
border: 2px solid transparent;
cursor: pointer;
transition: border-color 0.15s, transform 0.1s;
}
.ep-color-swatch:hover {
transform: scale(1.1);
}
.ep-color-swatch.active {
border-color: var(--ep-primary, #4f8cff);
}
.ep-color-picker {
width: 28px;
height: 28px;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 0;
background: none;
}
.ep-color-picker::-webkit-color-swatch-wrapper {
padding: 0;
}
.ep-color-picker::-webkit-color-swatch {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
@@ -196,6 +196,8 @@ const EditingPlanner: React.FC = () => {
{/* 右栏 260px:设置面板 */}
<RightPanel
titleConfig={titleConfig}
onTitleConfigChange={setTitleConfig}
rightTab={rightTab}
onTabChange={setRightTab}
selectedClip={clipOps.selectedClip}
@@ -5,12 +5,15 @@
import React from "react"
import type { ClipPropertiesPanelProps } from "@/pages/editing-planner/types/clipProperties"
import SubtitleSettingsSection from "./clip-properties/SubtitleSettingsSection"
import TitleSettingsSection from "./clip-properties/TitleSettingsSection"
import BgmSettingsSection from "./clip-properties/BgmSettingsSection"
import ClipDetailSection from "./clip-properties/ClipDetailSection"
import StatsSection from "./clip-properties/StatsSection"
import { useVoicePreview } from "@/pages/editing-planner/hooks/useVoicePreview"
const ClipPropertiesPanel: React.FC<ClipPropertiesPanelProps> = ({
titleConfig,
onTitleConfigChange,
selectedClip,
subtitleSettings,
bgmSettings,
@@ -40,6 +43,11 @@ const ClipPropertiesPanel: React.FC<ClipPropertiesPanelProps> = ({
return (
<div className="ep-right-panel">
{/* ═══ 标题设置 — #1789 ═══ */}
{titleConfig && onTitleConfigChange && (
<TitleSettingsSection config={titleConfig} onChange={onTitleConfigChange} />
)}
{/* ═══ 字幕设置 ═══ */}
<SubtitleSettingsSection
settings={subtitleSettings}
@@ -5,9 +5,12 @@ import type { ClipData } from "../types"
import type { SubtitleStyleConfig } from "../types/subtitle"
import type { BgmMixConfig } from "@/api/bgm"
import type { TemplateMode } from "@/api/editing-planner"
import type { TitleConfig } from "@/api/template-editor"
import type { AssetItem } from "@/api/assets"
interface RightPanelProps {
titleConfig?: TitleConfig
onTitleConfigChange?: (config: TitleConfig | ((prev: TitleConfig) => TitleConfig)) => void
rightTab: "properties" | "clips"
onTabChange: (tab: "properties" | "clips") => void
// 属性 tab
@@ -46,6 +49,8 @@ interface RightPanelProps {
}
const RightPanel: React.FC<RightPanelProps> = ({
titleConfig,
onTitleConfigChange,
rightTab,
onTabChange,
selectedClip,
@@ -118,6 +123,8 @@ const RightPanel: React.FC<RightPanelProps> = ({
>["onBgmSettingsChange"]
return (
<ClipPropertiesPanel
titleConfig={titleConfig}
onTitleConfigChange={onTitleConfigChange}
selectedClip={selectedClip}
subtitleSettings={sub}
bgmSettings={bgm}
@@ -0,0 +1,136 @@
/**
* 标题设置区块 — #1789
* 提供字号滑块、字体预设、位置、颜色等控制入口
*/
import React from "react"
import type { TitleConfig } from "@/api/template-editor"
import { POSITION_OPTIONS, FONT_OPTIONS } from "@/pages/editing-planner/constants/clipProperties"
interface TitleSettingsSectionProps {
config: TitleConfig
onChange: (config: TitleConfig | ((prev: TitleConfig) => TitleConfig)) => void
}
const TITLE_COLOR_PRESETS = [
"#ffffff",
"#000000",
"#ff4444",
"#ffaa00",
"#44ff44",
"#4488ff",
"#ff44ff",
"#ffff44",
]
const TitleSettingsSection: React.FC<TitleSettingsSectionProps> = ({ config, onChange }) => {
const update = (partial: Partial<TitleConfig>) => {
onChange((prev: TitleConfig) => ({ ...prev, ...partial }))
}
return (
<div className="ep-settings-section">
<div className="ep-section-title">
<span className="ep-section-icon">📝</span>
标题设置
</div>
{/* AI 自动选择开关 */}
<div className="ep-toggle-row">
<span className="ep-toggle-label">AI 自动选择</span>
<div
className={`ep-toggle ${config.ai_auto_select ? "active" : ""}`}
onClick={() => update({ ai_auto_select: !config.ai_auto_select })}
>
<div className="ep-toggle-knob" />
</div>
</div>
{!config.ai_auto_select && (
<>
{/* 标题文本 */}
<div className="ep-field">
<label className="ep-field-label">标题文本</label>
<input
className="ep-form-select"
type="text"
placeholder="输入标题内容"
value={config.content}
onChange={(e) => update({ content: e.target.value })}
/>
</div>
{/* 位置 */}
<div className="ep-field">
<label className="ep-field-label">位置</label>
<select
className="ep-form-select"
value={config.position}
onChange={(e) => update({ position: e.target.value })}
>
{POSITION_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
{/* 字体预设 */}
<div className="ep-field">
<label className="ep-field-label">字体</label>
<select
className="ep-form-select"
value={config.font_preset}
onChange={(e) => update({ font_preset: e.target.value })}
>
{FONT_OPTIONS.map((f) => (
<option key={f} value={f}>
{f}
</option>
))}
</select>
</div>
{/* 字号滑块 */}
<div className="ep-field">
<label className="ep-field-label">字号</label>
<div className="ep-slider-row">
<input
className="ep-slider"
type="range"
min={12}
max={72}
value={config.font_size}
onChange={(e) => update({ font_size: Number(e.target.value) })}
/>
<span className="ep-slider-value">{config.font_size}px</span>
</div>
</div>
{/* 颜色 */}
<div className="ep-field">
<label className="ep-field-label">颜色</label>
<div className="ep-color-presets">
{TITLE_COLOR_PRESETS.map((color) => (
<div
key={color}
className={`ep-color-swatch${config.font_color === color ? " active" : ""}`}
style={{ backgroundColor: color }}
onClick={() => update({ font_color: color })}
/>
))}
<input
type="color"
className="ep-color-picker"
value={config.font_color}
onChange={(e) => update({ font_color: e.target.value })}
/>
</div>
</div>
</>
)}
</div>
)
}
export default TitleSettingsSection
@@ -108,7 +108,10 @@ export function usePlanLoading({
id: `seg-${idx}`,
template_segment_id: `seg-${idx}`,
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
duration: (seg.duration_min + seg.duration_max) / 2,
duration:
seg.duration_min != null && seg.duration_max != null
? (seg.duration_min + seg.duration_max) / 2
: 5,
startOffset: 0,
script_text: "",
order: seg.segment_order,
@@ -43,7 +43,10 @@ export function useTemplateDetail({
id: seg.id || `seg-${idx}`,
template_segment_id: seg.id || `seg-${idx}`,
type: (seg.material_type === "voiceover" ? "voice" : "pip") as ClipType,
duration: (seg.duration_min + seg.duration_max) / 2,
duration:
seg.duration_min != null && seg.duration_max != null
? (seg.duration_min + seg.duration_max) / 2
: 5,
startOffset: 0,
script_text: "",
order: seg.segment_order,
@@ -4,6 +4,7 @@
import type { ClipData } from "./clip"
import type { TemplateMode } from "@/api/editing-planner"
import type { AssetItem } from "@/api/assets"
import type { TitleConfig } from "@/api/template-editor"
export interface SubtitleSettings {
enabled: boolean
@@ -28,6 +29,10 @@ export interface BgmSettings {
}
export interface ClipPropertiesPanelProps {
/** 标题配置 — #1789 */
titleConfig?: TitleConfig
/** 标题配置变更 */
onTitleConfigChange?: (config: TitleConfig | ((prev: TitleConfig) => TitleConfig)) => void
selectedClip: ClipData | null
subtitleSettings: SubtitleSettings
bgmSettings: BgmSettings
@@ -13,6 +13,8 @@ export const formatTrimTime = (sec: number): string => {
/** 生成时间标尺刻度 */
export const generateRulerMarks = (totalDuration: number, step: number): number[] => {
const marks: number[] = []
// #1790: 无片段时不显示时间刻度
if (totalDuration <= 0) return marks
for (let t = 0; t <= totalDuration + step; t += step) {
marks.push(t)
}
+118 -43
View File
@@ -27,6 +27,8 @@ import { useStepNavigation } from "./hooks/useStepNavigation"
import { useGenerateVideo } from "./hooks/useGenerateVideo"
import { usePreviewAssets } from "./hooks/usePreviewAssets"
import { useBatchVariantPlans } from "./hooks/useBatchVariantPlans"
import { useVariantVoicePreview } from "./hooks/useVariantVoicePreview"
import { useTitleStyleUpdaters } from "./hooks/useStep4Title/useTitleStyleUpdaters"
import { getAssetsByKind } from "@/api/assets"
import { previewTts } from "@/api/tts"
@@ -43,6 +45,7 @@ const GeneratePage: React.FC = () => {
selectedTemplate,
setSelectedTemplate,
userTemplates,
handleInvalidTemplate,
selectedMaterials,
setSelectedMaterials,
materialMode,
@@ -136,14 +139,17 @@ const GeneratePage: React.FC = () => {
const variant0Title = isBatch ? previewTitles?.[0] || "" : ""
useEffect(() => {
// #1750:批量模式每变体独立/共用配音由 useVariantVoicePreview 接管(含独立模式每卡片挂各自 URL),
// 本 effect 只服务单视频,批量模式直接跳过(避免共用 TTS 结果错误下发给所有变体)
if (isBatch) return
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice)
if (voiceAsset?.file_url) {
setPreviewVoiceAudioUrl(voiceAsset.file_url)
return
}
// 批量模式下 TTS 文案跟随变体0标题;单视频跟随主标题
const ttsTitle = isBatch ? variant0Title || "" : titleSettings.title
// 单视频 TTS 文案跟随主标题
const ttsTitle = titleSettings.title
const voiceId = selectedClonedVoice || selectedVoice
if (!voiceId || !ttsTitle) {
setPreviewVoiceAudioUrl(null)
@@ -213,14 +219,56 @@ const GeneratePage: React.FC = () => {
/* ── 加载素材详情(供前端预览播放器使用) ── */
const previewAssetsEnabled = previewAssetIds.length > 0
const { assets: previewAssets, ready: previewAssetsReady } = usePreviewAssets(
previewAssetIds,
previewAssetsEnabled,
)
const {
assets: previewAssets,
ready: previewAssetsReady,
ensureAssets,
} = usePreviewAssets(previewAssetIds, previewAssetsEnabled)
/* ── 预览就绪:纯前端 Canvas 预览,素材详情加载完即可秒开(单视频/批量一致) ── */
const previewReady = previewAssetsReady && !!currentTemplate
/* ── 批量变体真实片段(#1744):后端独立选片,预览即成片;失败静默降级本地模拟 ──
仅批量(N>1)且在第 4 步预览时申请,避免选素材阶段频繁请求;
变体 0 沿用草稿 plan(与单视频一致),变体 1..N-1 后端 reselect 独立选片 */
const {
clipsByVariant: variantClips,
planIdsByVariant: variantPlanIds,
loading: variantClipsLoading,
error: variantClipsError,
retry: retryVariantClips,
} = useBatchVariantPlans({
enabled: isBatch && currentStep === 4 && previewAssetsReady,
count: previewCount,
templateId: selectedTemplate || "",
assetIds: previewAssetIds,
sourcePlanId: storedSourceEditPlanId || sourceEditPlanId || "",
})
/* ── 批量变体配音预览 URL(#1750):独立模式每变体挂各自配音,共用模式全挂同一条;
hook 内部自取配音素材库并做 TTS 缓存/串行/防竞态 */
const variantVoiceAudioUrls = useVariantVoicePreview({
enabled: isBatch,
count: previewCount,
perVideo: voiceModePerVideo,
sharedVoiceId: selectedVoice || "",
clonedVoiceId: selectedClonedVoice || "",
variantVoiceIds: voiceLibraryIds || [],
titles: previewTitles || [],
})
/* ── 变体计划 clips 引用的素材可能不在已选列表中(#1750):差集补拉素材详情,
保证播放器拿得到 file_url,而不是静默丢片段/用假数据冒充 */
const clipAssetIds = useMemo(() => {
if (!isBatch || !variantClips?.length) return []
const ids = new Set<string>()
variantClips.forEach((list) => list.forEach((c) => c.asset_id && ids.add(c.asset_id)))
return Array.from(ids)
}, [isBatch, variantClips])
useEffect(() => {
if (clipAssetIds.length > 0) void ensureAssets(clipAssetIds)
}, [clipAssetIds, ensureAssets])
/* ── 勾选变体 ── */
const toggleVariantSelect = useCallback(
(index: number) => {
@@ -262,6 +310,8 @@ const GeneratePage: React.FC = () => {
autoSubtitles,
bgm,
sourceEditPlanId: storedSourceEditPlanId || sourceEditPlanId,
// #1744:批量预览阶段后端为每个变体生成的独立 plan id,正式生成回传 → 预览即成片
variantPlanIds,
previewTaskId,
bgmConfig,
previewCount,
@@ -367,13 +417,11 @@ const GeneratePage: React.FC = () => {
/* ── 最终成片(单视频右侧播放) ── */
const finalVideo = generatedVideos[0]
/* ── 布局 class:步骤4标题页=预览+标题侧栏;步骤5/6批量=整行宽;步骤1~3=整行宽 ── */
/* ── 布局 class:步骤4标题页=预览+标题侧栏两栏;其余步骤(含步骤5确认生成、步骤6封面)=整行宽 ── */
const layoutClassName = useMemo(() => {
if (currentStep < 4) return "xx-generate-layout full-width"
if (currentStep === 4) return "xx-generate-layout step4-layout"
// 步骤5/6:批量网格需要整行宽度;单视频保持 表单+右侧成片 两栏
return isBatch ? "xx-generate-layout full-width" : "xx-generate-layout"
}, [currentStep, isBatch])
return "xx-generate-layout full-width"
}, [currentStep])
/* ================================================================
渲染
@@ -393,7 +441,6 @@ const GeneratePage: React.FC = () => {
/* 单视频:前端 Canvas 实时预览(与旧版一致,零回归) */
<FrontendPreviewPlayer
assets={previewAssets}
template={currentTemplate}
videoRatio={videoRatio}
ready={previewAssets.length > 0}
serverClips={serverClips}
@@ -425,11 +472,14 @@ const GeneratePage: React.FC = () => {
<CanvasPreviewGrid
count={previewCount}
assets={previewAssets}
template={currentTemplate}
videoRatio={videoRatio}
titles={previewTitles}
titleSettings={titleSettings}
voiceAudioUrl={previewVoiceAudioUrl || undefined}
voiceAudioUrls={variantVoiceAudioUrls}
variantClips={variantClips}
clipsLoading={variantClipsLoading}
clipsError={variantClipsError}
onRetryClips={retryVariantClips}
selectedIds={selectedVariantIds}
onToggleSelect={toggleVariantSelect}
selectable={!generating}
@@ -471,6 +521,7 @@ const GeneratePage: React.FC = () => {
selectedVoice={selectedVoice}
onSelectedVoiceChange={setSelectedVoice}
onServerClipsChange={setServerClips}
onTemplateInvalid={handleInvalidTemplate}
generating={generating}
generated={generated}
generateError={generateError}
@@ -492,6 +543,59 @@ const GeneratePage: React.FC = () => {
selectedVariantIds={selectedVariantIds}
/>
{/* ════ 步骤5(单视频):成片播放器置于按钮上方、居中展示 ════ */}
{currentStep === 5 && !isBatch && generated && finalVideo && (
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: 16,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
background: "#000",
borderRadius: 12,
padding: 8,
maxWidth: 320,
width: "100%",
}}
>
<video
src={finalVideo.download_url || finalVideo.file_url}
controls
autoPlay
style={{
width: "auto",
maxWidth: "100%",
maxHeight: "70vh",
aspectRatio: "9 / 16",
objectFit: "contain",
borderRadius: 8,
}}
poster={finalVideo.thumbnail_url || undefined}
/>
<div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }}>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleDownload}>
⬇️ 下载
</button>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleShare}>
🔗 分享
</button>
<button
className="xx-btn xx-btn-ghost xx-btn-sm"
onClick={() => navigate("/app/products")}
>
📁 前往成片库
</button>
</div>
</div>
</div>
)}
<GenerateStepActions
currentStep={currentStep}
onPrev={goPrev}
@@ -503,35 +607,6 @@ const GeneratePage: React.FC = () => {
selectedCount={isBatch ? selectedVariantIds.length : 1}
/>
</div>
{/* ════ 步骤5/6(单视频):右侧成片播放器 ════ */}
{currentStep >= 5 && !isBatch && generated && finalVideo && (
<div className="xx-generate-right-col">
<div className="xx-inline-video-player">
<video
src={finalVideo.download_url || finalVideo.file_url}
controls
autoPlay={currentStep === 5}
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain", borderRadius: 12 }}
poster={finalVideo.thumbnail_url || undefined}
/>
<div style={{ display: "flex", gap: 8, marginTop: 12, justifyContent: "center" }}>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleDownload}>
⬇️ 下载
</button>
<button className="xx-btn xx-btn-ghost xx-btn-sm" onClick={handleShare}>
🔗 分享
</button>
<button
className="xx-btn xx-btn-ghost xx-btn-sm"
onClick={() => navigate("/app/products")}
>
📁 前往成片库
</button>
</div>
</div>
</div>
)}
</div>
{/* 数量选择弹窗 */}
@@ -34,12 +34,26 @@ const BatchGenerationGrid: React.FC<BatchGenerationGridProps> = ({
完成 {tasks.filter((t) => t.status === "completed").length} / {tasks.length}
</span>
</div>
<div className="xx-batch-gen-grid">
<div
className="xx-batch-gen-grid"
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 180px))",
justifyContent: "center",
justifyItems: "center",
gap: 14,
width: "100%",
}}
>
{sorted.map((task) => {
const title = titles[task.variantIndex] || `视频 ${task.variantIndex + 1}`
const video = (task.videos?.[0] || null) as GeneratedVideo | null
return (
<div key={task.taskId} className={`xx-batch-gen-card status-${task.status}`}>
<div
key={task.taskId}
className={`xx-batch-gen-card status-${task.status}`}
style={{ maxWidth: 240 }}
>
<div className="xx-batch-gen-card-head">
<span className="xx-batch-gen-card-title" title={title}>
{task.status === "completed" ? (
@@ -66,12 +80,32 @@ const BatchGenerationGrid: React.FC<BatchGenerationGridProps> = ({
</>
)}
{task.status === "completed" && video && (
<video
src={video.download_url || video.file_url}
controls
style={{ width: "100%", borderRadius: 8, background: "#000", maxHeight: 280 }}
poster={video.thumbnail_url}
/>
// 竖屏自适应容器(#1750):成片固定 1080×1920(9:16),
// 视频按真实宽高比 contain 显示,黑底居中,杜绝横屏播放器左右大黑边
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
background: "transparent",
borderRadius: 8,
padding: 0,
}}
>
<video
src={video.download_url || video.file_url}
controls
style={{
width: "100%",
maxHeight: "70vh",
aspectRatio: "9 / 16",
objectFit: "cover",
borderRadius: 4,
background: "transparent",
}}
poster={video.thumbnail_url}
/>
</div>
)}
{task.status === "completed" && !video && (
<div className="xx-batch-gen-card-done">✅ 已完成(成片可在下一步选择封面)</div>
@@ -1,27 +1,47 @@
/**
* 批量前端 Canvas 实时预览网格(Issue #1677 修正方案)
* 批量前端 Canvas 实时预览网格(Issue #1677 起,#1750 对齐基线:只播放后端真实计划)
*
* N 个 FrontendPreviewPlayer 网格排列:
* - 纯前端 Canvas + video 元素实时播放素材片段,不调任何后端渲染接口
* - variantSeed 让每个变体素材排布/起始点不同,画面有可见差异
* - 各自叠加独立标题浮层(variantTitle),标题样式全局共用
* - 勾选框决定提交时生成哪些变体
* - 纯前端 Canvas + video 元素实时播放素材片段,不调任何后端渲染接口;
* - #1744/#1750:片段只来自后端变体计划接口(POST /generation/variant-plans)的真实
* clips,与正式批量生成同源自 reselect_plan_for_variant,预览即成片;
* 接口失败/数据不完整 → 整网格显示错误态+重试(严禁本地假数据冒充预览);
* 加载中 → 9:16 占位防塌陷;
* - 各自叠加独立标题浮层(variantTitle),标题样式全局共用;
* - 勾选框决定提交时生成哪些变体;
* - 每个变体挂载各自配音 URL(独立模式 #1750)或共用同一条;播放互斥:
* 点击某卡片播放时其他卡片自动暂停,同一时刻只有一路声音(#1741)。
*/
import React from "react"
import React, { useState } from "react"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import type { EditPlanClip } from "@/api/template-editor"
import { LoadingOutlined, ReloadOutlined } from "@ant-design/icons"
import { Button } from "antd"
import type { TitleSettings } from "../types"
import FrontendPreviewPlayer from "./FrontendPreviewPlayer"
interface CanvasPreviewGridProps {
count: number
assets: AssetItem[]
template: EditingTemplate | null
videoRatio: string
titles: string[]
titleSettings: TitleSettings
/** 共用配音预览音频(仅第 1 个变体播放,避免多路音频重叠) */
voiceAudioUrl?: string
/**
* 各变体配音预览音频 URL(#1750:独立配音模式每变体一条;共用模式全为同一条;
* 元素为 null 表示该变体暂无音频(AI 音色 TTS 合成中))
*/
voiceAudioUrls?: (string | null)[]
/**
* 各变体的后端真实片段(#1744/#1750):长度=count。
* 仅 clipsLoading=false 且 clipsError=false 时才会传给播放器。
*/
variantClips?: EditPlanClip[][]
/** 是否正在向后端申请变体计划 */
clipsLoading?: boolean
/** 申请变体计划失败(端点未上线/网络错误/数据不完整):显示错误态,严禁假数据 */
clipsError?: boolean
/** 用户点击错误态「重试」 */
onRetryClips?: () => void
/** 勾选的变体序号 */
selectedIds: number[]
onToggleSelect: (index: number) => void
@@ -32,15 +52,21 @@ interface CanvasPreviewGridProps {
const CanvasPreviewGrid: React.FC<CanvasPreviewGridProps> = ({
count,
assets,
template,
videoRatio,
titles,
titleSettings,
voiceAudioUrl,
voiceAudioUrls,
variantClips,
clipsLoading = false,
clipsError = false,
onRetryClips,
selectedIds,
onToggleSelect,
selectable = true,
}) => {
// ── 播放互斥(#1741):同一时刻只有一个卡片持有播放权(token = 变体序号,0 起,#1750) ──
const [activePlayToken, setActivePlayToken] = useState<number | null>(null)
// count 上限已在源头 PreviewCountModal 的数量选择(1~MAX_PREVIEW_COUNT=10)clamp,
// 这里完整渲染所有变体,保证每个变体都有勾选/预览入口,UI 与数据不脱节
return (
@@ -64,29 +90,54 @@ const CanvasPreviewGrid: React.FC<CanvasPreviewGridProps> = ({
<span>视频 {i + 1}</span>
</label>
</div>
<FrontendPreviewPlayer
assets={assets}
template={template}
videoRatio={videoRatio}
ready={assets.length > 0}
variantSeed={i + 1}
variantTitle={titles[i] || ""}
voiceAudioUrl={i === 0 ? voiceAudioUrl : undefined}
compact
titleSettings={{
title: titles[i] || "",
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as "top" | "center" | "bottom" | "custom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
posX: titleSettings.posX,
posY: titleSettings.posY,
}}
/>
{clipsError ? (
// ── 错误态(#1750):9:16 占位防塌陷,不渲染任何播放器(严禁假数据) ──
<div className="xx-variant-clips-status" role="alert">
<span className="xx-variant-clips-error-text">预览加载失败,请重试</span>
{i === 0 && onRetryClips ? (
<Button
size="small"
icon={<ReloadOutlined />}
onClick={onRetryClips}
style={{ marginTop: 12 }}
>
重试
</Button>
) : null}
</div>
) : clipsLoading || !variantClips?.[i]?.length ? (
// ── 加载态:9:16 占位防塌陷 ──
<div className="xx-variant-clips-status" aria-label={`变体${i + 1}片段加载中`}>
<LoadingOutlined />
<span style={{ marginTop: 8 }}>独立选片中…</span>
</div>
) : (
<FrontendPreviewPlayer
assets={assets}
videoRatio={videoRatio}
ready={assets.length > 0}
playToken={i}
serverClips={variantClips[i]}
variantTitle={titles[i] || ""}
voiceAudioUrl={voiceAudioUrls?.[i] || undefined}
activePlayToken={activePlayToken}
onPlayTokenChange={setActivePlayToken}
compact
titleSettings={{
title: titles[i] || "",
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as "top" | "center" | "bottom" | "custom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
posX: titleSettings.posX,
posY: titleSettings.posY,
}}
/>
)}
</div>
)
})}
@@ -13,18 +13,19 @@ import {
PauseCircleOutlined,
SoundOutlined,
LoadingOutlined,
AudioOutlined,
AudioMutedOutlined,
} from "@ant-design/icons"
import type { AssetItem } from "@/api/assets"
import type { EditingTemplate } from "@/api/editing-planner"
import type { EditPlanClip } from "@/api/template-editor"
import { useSegmentScheduler, type PlaybackSegment } from "../hooks/useSegmentScheduler"
import { useCanvasPlayer } from "../hooks/useCanvasPlayer"
interface FrontendPreviewPlayerProps {
assets: AssetItem[]
template: EditingTemplate | null
videoRatio: string
ready: boolean
/** 服务端变体计划真实片段(#1750:必填,无 fallback;批量网格传入各变体自己的 clips) */
serverClips?: EditPlanClip[]
voiceAudioUrl?: string
titleSettings?: {
@@ -42,15 +43,21 @@ interface FrontendPreviewPlayerProps {
}
onTitlePositionChange?: (posX: number, posY: number) => void
/**
* 变体种子(批量生成 #1677):同一批素材在不同变体中采用不同的素材顺序与
* 片段起始点,让 N 个 Canvas 预览画面有差异(纯前端随机剪辑模拟,不调后端)。
* 0 / 不传 = 单视频,排布与旧版完全一致(零回归)。
* 播放互斥 token(#1750):批量网格中为变体序号(0 起),单视频不传。
* 持有播放权的实例播放,其余自动暂停。
*/
variantSeed?: number
playToken?: number
/** 变体标题文字(批量时每个预览独立标题,叠加在画面上);不传用 titleSettings.title */
variantTitle?: string
/** 紧凑模式(批量网格中使用,缩小内边距/标题尺寸) */
compact?: boolean
/**
* 批量网格播放互斥(#1741/#1750):当前持有播放权的实例 token(变体序号)。
* 持有权变化且不等于自身时,本实例自动暂停(视频+配音)。单视频模式不传。
*/
activePlayToken?: number | null
/** 播放权变化回调:本实例请求播放时传自身 playToken,暂停时传 null */
onPlayTokenChange?: (token: number | null) => void
}
function formatTime(seconds: number): string {
@@ -60,114 +67,107 @@ function formatTime(seconds: number): string {
}
/**
* 将素材映射为播放片段(复用原逻辑)
* 将后端变体计划 clips 映射为播放片段(#1750:唯一数据来源,无本地模拟 fallback)
*
* 预览不渲染:浏览器按后端真实计划即时播放素材原片,预览即成片。
* 模板片段数固定、成片时长=配音时长(后端等比分配),前端不再有任何时长假设。
*/
/** 简单可复现随机数(mulberry32),同一种子产出稳定排布,避免每次渲染抖动 */
function seededRandom(seed: number): () => number {
let a = seed >>> 0
return () => {
a |= 0
a = (a + 0x6d2b79f5) | 0
let t = Math.imul(a ^ (a >>> 15), 1 | a)
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
}
}
function buildPlaybackSegments(
assets: AssetItem[],
template: EditingTemplate | null,
serverClips?: EditPlanClip[],
variantSeed = 0,
/** #1754 前端兜底:配音时长≠clips 总时长时,按比例缩放每段播放时长并调速 */
speedFactor = 1,
): PlaybackSegment[] {
if (!assets.length) return []
if (!assets.length || !serverClips || serverClips.length === 0) return []
// Build asset lookup map
const assetMap = new Map(assets.map((a) => [a.id, a]))
// 优先使用服务端 clips(含随机 start_time 和正确数量),与最终生成结果一致
if (serverClips && serverClips.length > 0) {
const segments: PlaybackSegment[] = []
for (const clip of serverClips) {
const asset = assetMap.get(clip.asset_id)
if (!asset) continue
const assetDuration = asset.duration || asset.metadata?.duration || 30
const startTime = clip.start_time || 0
const endTime = Math.min(startTime + (clip.duration || assetDuration), assetDuration)
const videoUrl = asset.file_url || asset.storage_key
segments.push({ assetId: asset.id, videoUrl, startTime, endTime, order: clip.order })
}
if (segments.length > 0) {
return segments.sort((a, b) => a.order - b.order)
}
}
// Fallback: 本地构建片段
// variantSeed=0(单视频):与旧行为完全一致(素材原序、起始点 0),零回归
// variantSeed>0(批量变体):素材顺序按种子轮换 + 片段起始点在素材内偏移,
// 模拟后端"AI 随机剪辑出不同版本",让 N 个预览画面有可见差异
const templateSegments = template?.segments || []
const segments: PlaybackSegment[] = []
const orderedAssets = variantSeed > 0 ? [...assets] : assets
if (variantSeed > 0 && orderedAssets.length > 1) {
const rand = seededRandom(variantSeed * 7919 + 13)
// 素材轮换:把数组旋转 (seed % n) 位,再对后半段做一次稳定交换
const n = orderedAssets.length
const rotate = variantSeed % n
orderedAssets.push(...orderedAssets.splice(0, rotate))
const swapA = Math.floor(rand() * n)
const swapB = Math.floor(rand() * n)
if (swapA !== swapB) {
;[orderedAssets[swapA], orderedAssets[swapB]] = [orderedAssets[swapB], orderedAssets[swapA]]
}
}
orderedAssets.forEach((asset, i) => {
const assetDuration = asset.duration || asset.metadata?.duration || 30
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
const segDuration = tplSeg
? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration))
: Math.min(assetDuration, 10)
let startTime = 0
if (variantSeed > 0 && assetDuration - segDuration > 1) {
const rand = seededRandom(variantSeed * 104729 + i * 31 + 7)
// 起始点在素材可用区间内随机偏移(至少留 0.5s 余量)
const maxStart = Math.max(0, assetDuration - segDuration - 0.5)
startTime = Math.round(rand() * maxStart * 10) / 10
}
const endTime = Math.min(startTime + segDuration, assetDuration)
// speedFactor > 1 表示 clips 偏短需加速;< 1 表示 clips 偏长需减速
const invSpeed = speedFactor > 0 && Math.abs(speedFactor - 1) > 0.01 ? 1 / speedFactor : 1
for (const clip of serverClips) {
const asset = assetMap.get(clip.asset_id)
if (!asset) continue
const assetDuration = asset.duration || asset.metadata?.duration || 0
const startTime = clip.start_time || 0
// 片段时长以后端计划为准(配音时长等比分配);素材时长仅用于兜底钳制边界
const rawClipDuration = clip.duration || 0
// #1754:按 speedFactor 缩放片段时长,使总时长匹配配音
const clipDuration = invSpeed !== 1 ? rawClipDuration * invSpeed : rawClipDuration
const endTime =
assetDuration > 0
? Math.min(startTime + clipDuration, assetDuration)
: startTime + clipDuration
const videoUrl = asset.file_url || asset.storage_key
segments.push({ assetId: asset.id, videoUrl, startTime, endTime, order: i })
})
return segments
segments.push({
assetId: asset.id,
videoUrl,
startTime,
endTime,
order: clip.order,
playbackRate: invSpeed !== 1 ? speedFactor : undefined,
})
}
return segments.sort((a, b) => a.order - b.order)
}
const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
assets,
template,
videoRatio,
ready,
serverClips,
voiceAudioUrl,
titleSettings,
onTitlePositionChange,
variantSeed = 0,
playToken,
variantTitle,
compact = false,
activePlayToken = null,
onPlayTokenChange,
}) => {
// #1754:测量配音时长,计算缩放因子
const [voiceDuration, setVoiceDuration] = useState(0)
useEffect(() => {
if (!voiceAudioUrl) {
setVoiceDuration(0)
return
}
const audio = new Audio()
audio.preload = "metadata"
const onLoaded = () => {
if (audio.duration && isFinite(audio.duration)) {
setVoiceDuration(audio.duration)
}
}
audio.addEventListener("loadedmetadata", onLoaded)
audio.src = voiceAudioUrl
return () => {
audio.removeEventListener("loadedmetadata", onLoaded)
}
}, [voiceAudioUrl])
// #1756:clips 原始总时长 + 转场时长(后端等比分配配音时包含转场占位)
const rawClipsDuration = useMemo(() => {
if (!serverClips?.length) return 0
return serverClips.reduce((sum, c) => sum + (c.duration || 0) + (c.transition_duration || 0), 0)
}, [serverClips])
// #1754→#1756:配音时长可用且与 clips+转场 总时长偏差 > 5% 时,按比例调速
const speedFactor = useMemo(() => {
if (!voiceDuration || voiceDuration <= 0 || rawClipsDuration <= 0) return 1
const ratio = rawClipsDuration / voiceDuration
return Math.abs(ratio - 1) > 0.05 ? ratio : 1
}, [voiceDuration, rawClipsDuration])
const segments = useMemo(
() => buildPlaybackSegments(assets, template, serverClips, variantSeed),
[assets, template, serverClips, variantSeed],
() => buildPlaybackSegments(assets, serverClips, speedFactor),
[assets, serverClips, speedFactor],
)
// 批量变体:标题文字取 variantTitle,样式仍由全局 titleSettings 控制
const effectiveTitle = variantTitle ?? titleSettings?.title
// ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ──
const TITLE_MARGIN_TOP = 120
const TITLE_MARGIN_BOTTOM = 60
const TITLE_MARGIN_TOP = 180
const TITLE_MARGIN_BOTTOM = 100
const TITLE_MARGIN_SIDE = 40
const playRes = (() => {
switch (videoRatio) {
@@ -339,6 +339,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
canPlay: videoCanPlay,
togglePlayPause: videoTogglePlayPause,
seekTo: videoSeekTo,
pause: videoPause,
videoRefs,
} = useSegmentScheduler(segments)
@@ -353,6 +354,10 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
// ── 配音音频同步 ──
const audioRef = useRef<HTMLAudioElement | null>(null)
const prevIsPlayingRef = useRef(false)
// 本卡片静音开关(#1741):默认有声,用户可点喇叭单独静音某张卡片
const [muted, setMuted] = useState(false)
// 有配音时 video 素材保持静音(避免原声与配音混音);无配音时取消静音,素材原声兜底
const hasVoice = !!voiceAudioUrl
useEffect(() => {
if (!voiceAudioUrl) {
@@ -370,7 +375,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
if (audioRef.current.src !== voiceAudioUrl) {
audioRef.current.src = voiceAudioUrl
}
}, [voiceAudioUrl])
audioRef.current.muted = muted
}, [voiceAudioUrl, muted])
useEffect(() => {
const audio = audioRef.current
@@ -409,17 +415,42 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
[effectiveUseWebCodecs, canvasControls, videoSeekTo],
)
// ── 批量网格播放互斥(#1741):播放权属于其他实例时,本实例自动暂停(视频+配音) ──
useEffect(() => {
if (activePlayToken == null || playToken == null || activePlayToken === playToken) return
if (effectiveUseWebCodecs) {
if (canvasState.isPlaying) canvasControls.pause()
} else if (isPlaying) {
videoPause()
}
// isPlaying/canvasState.isPlaying 不放依赖:只在 token 变化时执行一次暂停,
// token 等于自身时本实例的播放在 handleTogglePlay 里处理
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activePlayToken, playToken, effectiveUseWebCodecs])
const handleTogglePlay = useCallback(() => {
if (effectiveUseWebCodecs) {
if (canvasState.isPlaying) {
canvasControls.pause()
onPlayTokenChange?.(null)
} else {
if (playToken != null) onPlayTokenChange?.(playToken)
canvasControls.play()
}
} else {
// video fallback:先上报播放权(暂停其他卡片),再切换本卡片播放/暂停
if (playToken != null) onPlayTokenChange?.(isPlaying ? null : playToken)
videoTogglePlayPause()
}
}, [effectiveUseWebCodecs, canvasState.isPlaying, canvasControls, videoTogglePlayPause])
}, [
effectiveUseWebCodecs,
canvasState.isPlaying,
canvasControls,
videoTogglePlayPause,
isPlaying,
playToken,
onPlayTokenChange,
])
// ── 进度条拖拽 ──
const [isDragging, setIsDragging] = useState(false)
@@ -573,7 +604,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
maxWidth: compact ? "100%" : 280,
margin: compact ? 0 : "0 auto",
aspectRatio: "9 / 16",
background: "#0a0a0a",
background: compact ? "transparent" : "#0a0a0a",
borderRadius: compact ? 10 : 24,
overflow: "hidden",
boxShadow: compact
@@ -597,7 +628,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
style={{
width: "100%",
height: "100%",
objectFit: "contain",
objectFit: "cover",
}}
/>
</div>
@@ -608,7 +639,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
segments.map((seg, i) => (
<video
key={seg.assetId}
muted
muted={hasVoice || muted}
ref={(el) => {
videoRefs.current[i] = el
}}
@@ -619,7 +650,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
inset: 0,
width: "100%",
height: "100%",
objectFit: "contain",
objectFit: "cover",
background: "#000",
zIndex: 1,
opacity: i === videoCurrentSegIdx ? 1 : 0,
@@ -662,8 +693,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
? { top: "50%", transform: "translate(-50%, -50%)" }
: { bottom: `${titleBottomPct}%` }),
}),
pointerEvents: onTitlePositionChange && variantSeed === 0 ? "auto" : "none",
cursor: onTitlePositionChange && variantSeed === 0 ? "grab" : "default",
pointerEvents: onTitlePositionChange && playToken == null ? "auto" : "none",
cursor: onTitlePositionChange && playToken == null ? "grab" : "default",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
@@ -743,6 +774,45 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
</button>
)}
{/* 静音/有声切换(#1741):左上角,默认有声;批量与单视频均可单独静音 */}
{segments.length > 0 && (
<button
type="button"
aria-label={muted ? "取消静音" : "静音"}
title={muted ? "取消静音" : "静音"}
onClick={() => setMuted((m) => !m)}
style={{
position: "absolute",
top: 8,
left: 8,
width: compact ? 26 : 30,
height: compact ? 26 : 30,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(0,0,0,0.45)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: "50%",
color: muted ? "rgba(255,255,255,0.45)" : "rgba(255,255,255,0.92)",
fontSize: compact ? 13 : 15,
cursor: "pointer",
zIndex: 10,
padding: 0,
transition: "background 0.15s, color 0.15s",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = "rgba(0,0,0,0.65)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "rgba(0,0,0,0.45)"
}}
>
{muted ? <AudioMutedOutlined /> : <AudioOutlined />}
</button>
)}
{/* 片段指示器 — 右上角胶囊 */}
<div
style={{
@@ -753,9 +823,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
color: "rgba(255,255,255,0.9)",
fontSize: 10,
fontSize: compact ? 9 : 10,
fontWeight: 500,
padding: "2px 8px",
padding: compact ? "1px 6px" : "2px 8px",
borderRadius: 999,
zIndex: 10,
border: "1px solid rgba(255,255,255,0.1)",
@@ -774,8 +844,8 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
right: 0,
display: "flex",
alignItems: "center",
gap: 10,
padding: "12px 16px 16px",
gap: compact ? 6 : 10,
padding: compact ? "8px 10px 10px" : "12px 16px 16px",
background: "linear-gradient(transparent, rgba(0,0,0,0.7))",
backdropFilter: "blur(4px)",
WebkitBackdropFilter: "blur(4px)",
@@ -788,10 +858,10 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
background: "rgba(255,255,255,0.15)",
border: "none",
color: "#fff",
fontSize: 16,
fontSize: compact ? 14 : 16,
cursor: "pointer",
width: 32,
height: 32,
width: compact ? 26 : 32,
height: compact ? 26 : 32,
borderRadius: "50%",
display: "flex",
alignItems: "center",
@@ -811,9 +881,9 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
<span
style={{
fontSize: 11,
fontSize: compact ? 10 : 11,
color: "rgba(255,255,255,0.85)",
minWidth: 72,
minWidth: compact ? 58 : 72,
fontVariantNumeric: "tabular-nums",
letterSpacing: 0.2,
}}
@@ -1,96 +0,0 @@
import React from "react"
import { Typography } from "antd"
import { PlayCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"
import type { GeneratedVideo } from "@/api/template-editor"
import { ProgressIndicator } from "./result-panel/ProgressIndicator"
import { ResultVideoCard } from "./result-panel/ResultVideoCard"
const { Text } = Typography
interface GenerateResultPanelProps {
generated: boolean
generating: boolean
progress: number
generateError: string | null
generatedVideos: GeneratedVideo[]
onVideoPreview: (video: GeneratedVideo) => void
onDownload: () => void
onShare: () => void
onGoToLibrary: () => void
}
const GenerateResultPanel: React.FC<GenerateResultPanelProps> = ({
generated,
generating,
progress,
generateError,
generatedVideos,
onVideoPreview,
onDownload,
onShare,
onGoToLibrary,
}) => {
return (
<div className="xx-generate-result">
<div className="xx-result-header">
<h3>生成结果</h3>
{generated && generatedVideos.length > 0 && (
<span className="xx-result-count">{generatedVideos.length} 个视频</span>
)}
</div>
{generating && <ProgressIndicator progress={progress} />}
{generateError && !generating && (
<div className="xx-result-empty">
<CloseCircleOutlined style={{ fontSize: 40, color: "#ff4d4f", marginBottom: 12 }} />
<Text strong style={{ display: "block", marginBottom: 4 }}>
生成失败
</Text>
<Text style={{ fontSize: 12, color: "var(--text-secondary)" }}>
{typeof generateError === "string" ? generateError : "请重试"}
</Text>
</div>
)}
{!generated && !generating && !generateError && (
<div className="xx-result-empty">
<PlayCircleOutlined
style={{ fontSize: 48, color: "var(--text-tertiary)", marginBottom: 12 }}
/>
<Text style={{ color: "var(--text-secondary)", fontSize: 13 }}>
完成配置后点击「确认生成」
</Text>
<Text style={{ color: "var(--text-tertiary)", fontSize: 12, marginTop: 4 }}>
生成的视频将在这里展示
</Text>
</div>
)}
{generated && generatedVideos.length > 0 && (
<div className="xx-video-grid">
{generatedVideos.map((video, idx) => (
<ResultVideoCard
key={video.id || idx}
video={video}
index={idx}
onPreview={onVideoPreview}
onDownload={onDownload}
onShare={onShare}
/>
))}
</div>
)}
{generated && (
<div className="xx-result-footer">
<button className="xx-btn xx-btn-ghost xx-btn-block" onClick={onGoToLibrary}>
前往成片库 →
</button>
</div>
)}
</div>
)
}
export default GenerateResultPanel
@@ -50,6 +50,8 @@ export interface GenerateStepContentProps {
selectedVoice: string
onSelectedVoiceChange: (id: string) => void
onServerClipsChange: (clips: EditPlanClip[]) => void
/** 当前模板创建片段被判失效(404/400/422)时的自动回退回调(#1777) */
onTemplateInvalid?: () => boolean
/* 生成 */
generating: boolean
generated: boolean
@@ -108,6 +110,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
selectedVoice,
onSelectedVoiceChange,
onServerClipsChange,
onTemplateInvalid,
generating,
generated,
generateError,
@@ -153,6 +156,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
selectedTemplate={selectedTemplate}
templateSegments={templateSegments}
onServerClipsChange={onServerClipsChange}
onTemplateInvalid={onTemplateInvalid}
/>
)
case 3:
@@ -189,7 +193,7 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
/>
)
case 5:
/* 确认生成页:批量=逐任务进度网格;单视频=进度状态卡(成片播放器在左侧大区域) */
/* 确认生成页:批量=逐任务进度网格;单视频=仅渲染进度/失败状态(完成后只显示成片播放器,播放器在按钮上方) */
if (previewCount > 1) {
return (
<BatchGenerationGrid
@@ -199,10 +203,10 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
/>
)
}
/* 单视频:渲染进度 / 失败重试 / 完成提示(成片播放器在右侧栏) */
/* 单视频:生成中显示进度卡、失败显示重试卡;生成完成后不再渲染提示卡,页面只保留成片播放器+操作按钮 */
if (generated && !generating && !generateError) return null
return (
<div className="xx-form-section">
<h3>🎬 确认生成</h3>
{generating && (
<div className="xx-gen-progress-card">
<div className="xx-gen-progress-header">
@@ -234,14 +238,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
</button>
</div>
)}
{generated && !generating && (
<div className="xx-gen-success-card">
<div className="xx-gen-success-info">
<div className="xx-gen-success-title">✅ 视频生成完成!</div>
<div className="xx-gen-success-sub">右侧可预览成片,点击「下一步」选择封面</div>
</div>
</div>
)}
</div>
)
case 6:
@@ -1,380 +0,0 @@
/**
* 右侧预览视频面板 — 服务器渲染预览架构
*
* Step4+: 显示预览面板
* Step5: 播放服务器渲染的真实视频(POST /generation/preview)
*
* 架构:
* - 进入 Step4/5 时自动创建服务器预览渲染任务
* - 轮询完成后用 <video> 标签播放返回的 video_url
* - 标题样式编辑时 CSS TitleOverlay 实时叠加预览
* - 素材/配音/BGM 变更自动重新渲染
* - 标题文字/样式变更标记 stale,保留旧视频 + 显示"重新预览"按钮
*
* 点"确认生成"时走 confirm 路径,成品就是预览视频本身,100% 一致。
*/
import React, { useMemo, useRef, useState, useEffect } from "react"
import { LoadingOutlined, ReloadOutlined, ExclamationCircleOutlined } from "@ant-design/icons"
import { Button } from "antd"
import type { TitleSettings } from "../types"
import { getFontFamily } from "../constants"
import type { ServerPreviewStatus } from "../hooks/useServerPreview"
interface PreviewVideoPanelProps {
/** 服务器预览状态 */
previewStatus: ServerPreviewStatus
/** 服务器渲染视频 URL */
videoUrl: string | null
/** 渲染进度 0-100 */
progress: number
/** 错误信息 */
error: string | null
/** 重新预览回调 */
onRetry: () => void
/** 视频比例 */
videoRatio: string
/** 标题设置 — CSS 实时预览层 */
titleSettings?: TitleSettings
/** 素材数量 */
assetCount?: number
}
/* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */
const TITLE_MARGIN_TOP = 120
const TITLE_MARGIN_BOTTOM = 60
const TITLE_MARGIN_SIDE = 40
/** 根据视频比例返回后端实际渲染分辨率(PlayResX × PlayResY) */
function getResolution(ratio: string): { width: number; height: number } {
switch (ratio) {
case "16:9":
return { width: 1920, height: 1080 }
case "1:1":
return { width: 1080, height: 1080 }
case "9:16":
default:
return { width: 1080, height: 1920 }
}
}
function getPositionStyle(
position: string,
playResX: number,
playResY: number,
): React.CSSProperties {
const sidePercent = (TITLE_MARGIN_SIDE / playResX) * 100
switch (position) {
case "bottom":
return {
bottom: `${(TITLE_MARGIN_BOTTOM / playResY) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
case "center":
return {
top: "50%",
transform: "translateY(-50%)",
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
case "top":
default:
return {
top: `${(TITLE_MARGIN_TOP / playResY) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
}
}
}
function buildTitleStyle(
settings: TitleSettings,
containerHeight: number,
playResY: number,
): React.CSSProperties {
// 字号按容器高度与 PlayResY 的比例缩放,不设上限(与后端一致)
const fontSizePx =
containerHeight > 0
? (settings.size / playResY) * containerHeight
: (settings.size / playResY) * 400
const base: React.CSSProperties = {
fontFamily: getFontFamily(settings.font),
fontSize: `${fontSizePx}px`,
color: settings.color || "#ffffff",
fontWeight: settings.bold ? 700 : 400,
fontStyle: settings.italic ? "italic" : "normal",
lineHeight: 1.05,
wordBreak: "break-word",
pointerEvents: "none",
userSelect: "none",
}
if (settings.stroke) base.WebkitTextStroke = "1px #000000"
if (settings.shadow) base.textShadow = "2px 2px 4px rgba(0,0,0,0.8)"
return base
}
/** CSS 标题实时预览覆盖层 */
const TitleOverlay: React.FC<{ titleSettings: TitleSettings; videoRatio: string }> = ({
titleSettings,
videoRatio,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(400)
useEffect(() => {
const el = containerRef.current
if (!el) return
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const h = entry.contentRect.height
if (h > 0) setContainerHeight(h)
}
})
ro.observe(el)
const rect = el.getBoundingClientRect()
if (rect.height > 0) setContainerHeight(rect.height)
return () => ro.disconnect()
}, [])
const { width: playResX, height: playResY } = getResolution(videoRatio)
const positionStyle = useMemo(
() => getPositionStyle(titleSettings.position, playResX, playResY),
// eslint-disable-next-line react-hooks/exhaustive-deps
[titleSettings.position, playResX, playResY],
)
const titleStyle = useMemo(
() => buildTitleStyle(titleSettings, containerHeight, playResY),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
containerHeight,
titleSettings.font,
titleSettings.size,
titleSettings.color,
titleSettings.bold,
titleSettings.italic,
titleSettings.stroke,
titleSettings.shadow,
playResY,
],
)
const displayTitle = titleSettings.title?.trim() || "标题预览"
return (
<div
ref={containerRef}
style={{
position: "absolute",
inset: 0,
zIndex: 20,
pointerEvents: "none",
overflow: "hidden",
}}
>
<div style={{ ...positionStyle, ...titleStyle, position: "absolute" }}>
{displayTitle.split(/[//]/).map((part, i) => (
<span key={i}>
{i > 0 && <br />}
{part}
</span>
))}
</div>
</div>
)
}
/* ── 主组件 ── */
export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
previewStatus,
videoUrl,
progress,
error,
onRetry,
videoRatio,
titleSettings,
assetCount,
}) => {
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 (
<div className="xx-generate-preview">
<div className="xx-preview-header">
<h3>预览视频</h3>
{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 className="xx-preview-video" style={{ ...videoAspectStyle, position: "relative" }}>
{/* 加载中 */}
{isLoading && (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
zIndex: 5,
background: "rgba(0,0,0,0.6)",
}}
>
<LoadingOutlined style={{ fontSize: 36, color: "#fff" }} spin />
<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>
</div>
)}
{/* 空闲状态(尚未触发预览) */}
{isIdle && (
<div
style={{
position: "absolute",
inset: 0,
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>
)}
{/* 服务器渲染的真实视频 */}
{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} videoRatio={videoRatio} />
)}
{/* 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>
{/* 素材信息 */}
{assetCount !== undefined && assetCount > 0 && (
<div className="xx-preview-info">
<div className="xx-preview-info-row">
<span>素材数</span>
<span>{assetCount} 个</span>
</div>
<div className="xx-preview-info-row">
<span>比例</span>
<span>{videoRatio}</span>
</div>
</div>
)}
</div>
)
}
export default PreviewVideoPanel
@@ -23,6 +23,8 @@ interface Step2MaterialSelectProps {
templateSegments?: TemplateSegment[]
/** 服务端 clips 创建成功后的回调 */
onServerClipsChange?: (clips: EditPlanClip[]) => void
/** 当前模板创建片段返回 404/400/422(模板失效)时的自动回退回调(#1777) */
onTemplateInvalid?: () => boolean
}
const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => {
@@ -36,16 +38,25 @@ const Step2MaterialSelect: React.FC<Step2MaterialSelectProps> = (props) => {
<div className="xx-form-field" style={{ marginTop: 12 }}>
<label>选择视频库</label>
<select
value={m.selectedLibraryId}
onChange={(e) => m.setSelectedLibraryId(e.target.value)}
>
{m.libraries.map((lib) => (
<option key={lib.id} value={lib.id}>
{lib.name}
</option>
))}
</select>
{m.libraries.length === 0 && !m.materialsLoading ? (
<div className="xx-empty-state">
<p>暂无视频素材库</p>
<p style={{ fontSize: 13, color: "var(--text-tertiary)" }}>
请先在「素材库」中创建视频素材库并上传视频
</p>
</div>
) : (
<select
value={m.selectedLibraryId}
onChange={(e) => m.setSelectedLibraryId(e.target.value)}
>
{m.libraries.map((lib) => (
<option key={lib.id} value={lib.id}>
{lib.name}
</option>
))}
</select>
)}
</div>
{m.materialMode === "manual" && (
@@ -37,6 +37,12 @@ const isAiVoice = (item: AssetItem): boolean => {
return (!duration || duration <= 0) && (!size || size <= 0)
}
/** 格式化时长(秒 → "14.7秒";0/无效 → "时长未知",不做任何时长过滤) */
const formatDurationSeconds = (seconds?: number): string => {
if (!seconds || seconds <= 0) return "时长未知"
return `${seconds.toFixed(1)}秒`
}
/** 格式化文件大小 */
const formatFileSize = (bytes?: number): string => {
if (!bytes || bytes <= 0) return "未知"
@@ -261,7 +267,9 @@ const Step5VoiceSelect: React.FC<Step5VoiceSelectProps> = ({
>
{isAiVoice(item) ? (
<span style={{ color: "#1677ff", fontWeight: 500 }}>AI 音色</span>
) : null}
) : (
<span>{formatDurationSeconds(getDuration(item))}</span>
)}
<span>{isAiVoice(item) ? "按文本合成" : formatFileSize(getFileSize(item))}</span>
</div>
</div>
@@ -91,11 +91,13 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
shadow: props.titleSettings?.shadow ?? false,
},
covers: batchCoversList,
onCoversChange: (urls) => {
// 按卡片顺序写回对应变体索引
onCoversChange: (updater) => {
// 按卡片顺序写回对应变体索引;支持函数式 updater(#1750:串行回写避免闭包覆盖)
const prevCardView = cardIndexes.map((vi) => (props.previewCovers || [])[vi] || "")
const nextCardView = typeof updater === "function" ? updater(prevCardView) : updater
const next = [...(props.previewCovers || [])]
cardIndexes.forEach((vi, cardPos) => {
next[vi] = urls[cardPos] || ""
next[vi] = nextCardView[cardPos] || ""
})
props.onPreviewCoversChange?.(next)
},
@@ -142,7 +144,8 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<Button
buttonType="primary"
onClick={() => void batchCovers.generateAll()}
disabled={completedVideos.length === 0 || batchCovers.loadingIndex !== null}
disabled={completedVideos.length === 0 || batchCovers.busyIndexes.length > 0}
style={{ whiteSpace: "nowrap", flexShrink: 0 }}
>
✨ 一键全部自动生成
</Button>
@@ -151,8 +154,8 @@ const Step6CoverSettings: React.FC<Step6CoverSettingsProps> = (props) => {
<div className="xx-cover-grid">
{cardIndexes.map((variantIndex, cardPos) => {
const url = batchCoversList[cardPos]
const isLoading = batchCovers.loadingIndex === cardPos
const isUploading = batchCovers.uploadingIndex === cardPos
const isLoading = batchCovers.busyIndexes.includes(cardPos)
const isUploading = batchCovers.uploadingIndexes.includes(cardPos)
const title = batchTitles[cardPos]
return (
<div className="xx-cover-card" key={variantIndex}>
@@ -1,40 +0,0 @@
import React from "react"
import { Typography } from "antd"
const { Text } = Typography
interface ProgressIndicatorProps {
progress: number
}
export const ProgressIndicator: React.FC<ProgressIndicatorProps> = ({ progress }) => {
return (
<div className="xx-result-progress">
<div className="xx-progress-circle">
<svg viewBox="0 0 80 80">
<circle cx="40" cy="40" r="36" fill="none" stroke="var(--border-color)" strokeWidth="6" />
<circle
cx="40"
cy="40"
r="36"
fill="none"
stroke="var(--primary-color)"
strokeWidth="6"
strokeDasharray={`${Math.round(progress) * 2.26} 226`}
strokeLinecap="round"
transform="rotate(-90 40 40)"
/>
</svg>
<span className="xx-progress-percent">{Math.round(progress)}%</span>
</div>
<div className="xx-progress-text">
<Text strong style={{ fontSize: 14, display: "block", marginBottom: 4 }}>
正在生成视频
</Text>
<Text style={{ fontSize: 12, color: "var(--text-secondary)" }}>
AI 正在处理素材,请稍候…
</Text>
</div>
</div>
)
}
@@ -1,63 +0,0 @@
import React from "react"
import { PlayCircleOutlined, DownloadOutlined, ShareAltOutlined } from "@ant-design/icons"
import type { GeneratedVideo } from "@/api/template-editor"
import { formatDuration } from "@/api/voice-clone"
interface ResultVideoCardProps {
video: GeneratedVideo
index: number
onPreview: (video: GeneratedVideo) => void
onDownload: () => void
onShare: () => void
}
export const ResultVideoCard: React.FC<ResultVideoCardProps> = ({
video,
index,
onPreview,
onDownload,
onShare,
}) => {
return (
<div className="xx-video-card" onClick={() => onPreview(video)}>
<div className="xx-video-thumb">
{video.thumbnail_url ? (
<img src={video.thumbnail_url} alt="" />
) : (
<div className="xx-video-thumb-placeholder">
<PlayCircleOutlined style={{ fontSize: 32, opacity: 0.5 }} />
</div>
)}
<div className="xx-video-play-overlay">
<PlayCircleOutlined style={{ fontSize: 36, color: "#fff" }} />
</div>
{video.duration && (
<span className="xx-video-duration">{formatDuration(video.duration)}</span>
)}
</div>
<div className="xx-video-info">
<div className="xx-video-title">视频 {index + 1}</div>
<div className="xx-video-actions">
<button
className="xx-video-action-btn"
onClick={(e) => {
e.stopPropagation()
onDownload()
}}
>
<DownloadOutlined />
</button>
<button
className="xx-video-action-btn"
onClick={(e) => {
e.stopPropagation()
onShare()
}}
>
<ShareAltOutlined />
</button>
</div>
</div>
</div>
)
}
@@ -1,117 +0,0 @@
import React from "react"
import { LoadingOutlined, CheckCircleFilled, CloseCircleOutlined } from "@ant-design/icons"
import type { GeneratedVideo } from "@/api/template-editor"
interface GenerationStatusProps {
generating: boolean
generated: boolean
generateError: string | null
progress: number
generatedVideos: GeneratedVideo[]
getGenerationPhase: (progress: number) => { icon: string; label: string }
onScrollToPreview: () => void
onRetry: () => void
onDismissError: () => void
}
const GenerationStatus: React.FC<GenerationStatusProps> = ({
generating,
generated,
generateError,
progress,
generatedVideos,
getGenerationPhase,
onScrollToPreview,
onRetry,
onDismissError,
}) => {
return (
<div style={{ marginTop: 16 }}>
{!generating && !generated && !generateError && (
<div className="xx-gen-progress-card" style={{ opacity: 0.85 }}>
<div className="xx-gen-progress-header">
<div className="xx-gen-progress-icon">🎬</div>
<div className="xx-gen-progress-info">
<div className="xx-gen-progress-phase">尚未开始生成视频</div>
<div className="xx-gen-progress-sub">
请返回「选择标题」步骤,点击「确认生成视频」开始渲染最终视频
</div>
</div>
</div>
</div>
)}
{generating && (
<div className="xx-gen-progress-card">
<div className="xx-gen-progress-header">
<div className="xx-gen-progress-icon">
<LoadingOutlined />
</div>
<div className="xx-gen-progress-info">
<div className="xx-gen-progress-phase">
{getGenerationPhase(progress).icon} {getGenerationPhase(progress).label}
</div>
<div className="xx-gen-progress-sub">预计还需 1-2 分钟,请稍候…</div>
</div>
<div className="xx-gen-progress-percent">{Math.round(progress)}%</div>
</div>
<div className="xx-gen-progress-bar">
<div
className="xx-gen-progress-bar-fill"
style={{ width: `${Math.min(Math.round(progress), 100)}%` }}
/>
</div>
<div className="xx-gen-progress-tip">
💡 生成过程中可以切换到其他页面操作,完成后会自动通知
</div>
</div>
)}
{generated && !generating && (
<div className="xx-gen-success-card">
<div className="xx-gen-success-icon">
<CheckCircleFilled style={{ fontSize: 32, color: "#52c41a" }} />
</div>
<div className="xx-gen-success-info">
<div className="xx-gen-success-title">视频生成完成!</div>
<div className="xx-gen-success-sub">
共生成 {generatedVideos.length} 条视频,可在右侧预览或前往成片库查看
</div>
</div>
<button
type="button"
className="xx-btn xx-btn-primary xx-btn-sm"
onClick={onScrollToPreview}
>
查看结果
</button>
</div>
)}
{generateError && !generating && (
<div className="xx-gen-error-card">
<div className="xx-gen-error-icon">
<CloseCircleOutlined style={{ fontSize: 28, color: "#ef4444" }} />
</div>
<div className="xx-gen-error-info">
<div className="xx-gen-error-title">生成失败</div>
<div className="xx-gen-error-msg">
{typeof generateError === "string" ? generateError : JSON.stringify(generateError)}
</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
<button type="button" className="xx-btn xx-btn-primary xx-btn-sm" onClick={onRetry}>
🔄 重试
</button>
<button
type="button"
className="xx-btn xx-btn-ghost xx-btn-sm"
onClick={onDismissError}
>
知道了
</button>
</div>
</div>
)}
</div>
)
}
export default GenerationStatus
+58 -38
View File
@@ -117,10 +117,6 @@
grid-template-columns: 1fr;
}
.xx-generate-layout.full-width .xx-generate-right-col {
display: none;
}
/* ============================================================
左侧表单区 generate-form
============================================================ */
@@ -2323,28 +2319,6 @@
生成结果(右侧)
================================================================ */
.xx-generate-right-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
/* ── 内联视频播放器(右侧) ── */
.xx-inline-video-player {
width: 100%;
max-width: 320px;
background: var(--bg-surface, #fff);
border: 1px solid var(--border-primary, #e2e8f0);
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.xx-inline-video-player video {
background: #000;
}
.xx-preview-header {
display: flex;
align-items: center;
@@ -2710,11 +2684,12 @@
/* ── 封面设置区域改造样式 ── */
/* 封面操作按钮区 */
/* 封面操作按钮区(单视频全宽页居中) */
.xx-cover-actions {
display: flex;
gap: 12px;
margin-bottom: 12px;
justify-content: center;
}
/* 已选模板文字 */
@@ -3202,10 +3177,11 @@
margin: 0;
}
/* ── 批量封面网格 ── */
/* ── 批量封面网格(单卡/少卡时居中排列,卡片限宽不拉伸) ── */
.xx-cover-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(180px, 220px));
justify-content: center;
gap: 16px;
}
@@ -3282,17 +3258,28 @@
/* ============================================================
批量前端 Canvas 预览网格(Issue #1677 修正:纯前端实时预览)
============================================================ */
/* #1741:卡片整体缩小至约 3/5——宽屏排 3 列(卡片限宽 220px 居中),
中屏自动回退 2 列,窄屏 1 列(见下方媒体查询);卡片保持 9:16 比例不变形 */
.xx-canvas-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(160px, 180px));
justify-content: center;
gap: 14px;
}
/* 窄屏单列时卡片限宽居中,避免 1fr 拉伸导致卡片过高 */
@media (max-width: 960px) {
.xx-canvas-grid {
grid-template-columns: minmax(0, 320px);
}
}
.xx-canvas-grid-card {
position: relative;
border: 2px solid var(--border-primary, #e2e8f0);
border-radius: 12px;
overflow: hidden;
background: #000;
background: transparent;
transition: border-color 0.2s ease;
min-width: 0;
}
@@ -3330,6 +3317,37 @@
accent-color: var(--primary-color, #1677ff);
}
/* #1750:批量变体片段加载/错误占位(9:16 竖屏比例,与播放器卡片同尺寸防塌陷) */
.xx-variant-clips-status {
position: relative;
width: 100%;
aspect-ratio: 9 / 16;
max-height: 70vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
background: #0a0a0a;
border-radius: 16px;
overflow: hidden;
box-shadow:
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);
}
.xx-variant-clips-status .anticon {
font-size: 22px;
}
.xx-variant-clips-error-text {
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
}
/* ============================================================
批量标题:AI 一键生成行(Issue #1677)
============================================================ */
@@ -3361,16 +3379,18 @@
第5步确认生成:批量渲染进度网格(Issue #1677)
============================================================ */
.xx-batch-gen-grid {
justify-items: center;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(160px, 180px));
justify-content: center;
gap: 14px;
}
.xx-batch-gen-card {
border: 1px solid var(--border-primary, #e2e8f0);
border-radius: 12px;
padding: 14px;
background: var(--bg-surface, #fff);
background: transparent;
display: flex;
flex-direction: column;
gap: 10px;
@@ -3435,10 +3455,10 @@
word-break: break-word;
}
/* ── 响应式:窄屏批量网格回退单列 ── */
/* ── 响应式:窄屏批量网格回退单列(.xx-canvas-grid 的窄屏限宽见网格定义处 #1741) ── */
@media (max-width: 960px) {
.xx-canvas-grid,
.xx-batch-gen-grid {
grid-template-columns: 1fr;
justify-items: center;
grid-template-columns: minmax(0, 320px);
}
}
@@ -42,6 +42,12 @@ export interface UseGenerateVideoProps {
variantCoverUrls?: string[]
/** 勾选要生成的变体索引(批量模式) */
selectedVariantIndexes?: number[]
/**
* 批量变体剪辑计划 ID(#1744,按变体全量索引长度=previewCount):
* 预览阶段后端独立选片产出,正式生成按勾选顺序回传,实现预览即成片;
* 为空(降级本地模拟/后端未上线)时不传,后端自行独立选片。
*/
variantPlanIds?: string[]
}
/** 生成阶段 */
@@ -8,11 +8,22 @@ import type { AssetItem } from "@/api/assets"
* 管理素材库列表、当前选中库、素材列表加载
*/
export function useMaterialLibrary() {
/* ── 素材库数据 API ── */
const { data: libraries = [] } = useQuery({
queryKey: ["asset-libraries"],
queryFn: getAssetLibraries,
/* ── 素材库数据 API ──
* Step2 是视频选片,只拉取 kind=video 的素材库(#1777):
* 后端按 kind 查询参数过滤,前端 getAssetLibraries("video") 再兜底过滤一次,
* 避免配音库(voice)/图片库(image) 混进「选择视频库」下拉。
* queryKey 带 kind,与素材管理页/配音页的 ["asset-libraries"] 全量缓存隔离。
*/
const { data: allLibraries = [] } = useQuery({
queryKey: ["asset-libraries", "video"],
queryFn: () => getAssetLibraries("video"),
staleTime: 60_000,
})
// 前端兜底过滤:仅保留 kind=video 的素材库(后端按 kind 查询参数过滤)
const libraries = useMemo(
() => allLibraries.filter((lib) => lib.kind === "video"),
[allLibraries],
)
const [selectedLibraryId, setSelectedLibraryId] = useState<string>("")
// 自动选中第一个视频库
@@ -13,21 +13,22 @@ interface UseSmartMatchOptions {
}
/** 默认 limit(拿不到目标时长时的兜底上限) */
const DEFAULT_LIMIT = 10
/** 每个素材切片按 15 秒估算所需素材数 */
const SECONDS_PER_ASSET = 15
const DEFAULT_LIMIT = 30
/**
* 候选池放大倍数(#1744):批量 N 个变体独立选片、跨变体 20% 区间避让需要
* 足够大的素材池才能保证 N 条成片素材排布互不相同(历史已用区间写回素材 metadata)。
* 旧规则「总时长/15 秒」只够 1 条成片的选片量,池太小会导致变体间大量复用同区间。
*/
const ASSET_POOL_MULTIPLIER = 3
/**
* 根据模板 segments 计算所需素材数量上限。
* 取每个 segment 的 duration_min 之和作为目标视频总时长,
* 再按 15 秒/素材估算需要多少个素材,且保证不少于片段数(每个片段至少 1 个素材);
* 结果钳制到 [1, 200] 区间(后端 limit 上限 200)。
* 根据模板 segments 计算智能匹配候选素材数量上限(#1750:模板无时长概念)。
* 候选池 = 片段数 × 3:每个片段至少 3 个候选素材供变体间洗牌/避让
* (独立选片 + 区间不重叠);结果钳制到 [1, 200] 区间(后端 limit 上限 200)。
*/
function computeLimitFromSegments(segments?: TemplateSegment[]): number {
export function computeLimitFromSegments(segments?: TemplateSegment[]): number {
if (!segments || segments.length === 0) return DEFAULT_LIMIT
const totalSeconds = segments.reduce((sum, seg) => sum + (seg.duration_min || 0), 0)
if (totalSeconds <= 0) return DEFAULT_LIMIT
const limit = Math.max(segments.length, Math.ceil(totalSeconds / SECONDS_PER_ASSET))
const limit = segments.length * ASSET_POOL_MULTIPLIER
return Math.max(1, Math.min(limit, 200))
}
@@ -1,6 +1,13 @@
/**
* 批量封面 Hook(Issue #1677)
* 批量封面 Hook(Issue #1677 / #1750 修复)
* N 个视频时:逐个自动生成封面(从对应成片抽帧 + 叠加对应标题)或上传自定义封面
*
* #1750 修复:
* - patchCover 改为函数式 updater 回写(onCoversChange 支持 (prev)=>next),
* 修复串行 generateAll 时闭包捕获旧 covers 数组导致回写互相覆盖的 bug;
* - loading/uploading 状态改数组(busyIndexes/uploadingIndexes),支持多个卡片在途;
* - generateOne 返回 boolean,generateAll 据此统计成功/失败数量分级提示,单个失败不阻塞;
* - generateAll 一次点击即处理所有未生成封面(无需点 3 次)。
*/
import { useCallback, useState } from "react"
import { message } from "antd"
@@ -8,6 +15,9 @@ import { generateCover } from "@/api/generation"
import { uploadAssetDirect, getAssetLibraries } from "@/api/assets"
import type { GeneratedVideo } from "@/api/template-editor"
/** onCoversChange 支持直接传值或函数式 updater(函数式用于串行回写避免闭包覆盖) */
type CoversChangeFn = (updater: string[] | ((prev: string[]) => string[])) => void
interface UseBatchCoversOptions {
selectedTemplate: string
generatedVideos: GeneratedVideo[]
@@ -24,7 +34,7 @@ interface UseBatchCoversOptions {
shadow: boolean
}
covers: string[]
onCoversChange: (urls: string[]) => void
onCoversChange: CoversChangeFn
}
export function useBatchCovers({
@@ -35,28 +45,46 @@ export function useBatchCovers({
covers,
onCoversChange,
}: UseBatchCoversOptions) {
const [loadingIndex, setLoadingIndex] = useState<number | null>(null)
const [uploadingIndex, setUploadingIndex] = useState<number | null>(null)
/** 正在自动生成封面的变体序号集合(数组支持多个卡片同时在途) */
const [busyIndexes, setBusyIndexes] = useState<number[]>([])
/** 正在上传自定义封面的变体序号集合 */
const [uploadingIndexes, setUploadingIndexes] = useState<number[]>([])
const addBusy = useCallback((i: number) => {
setBusyIndexes((prev) => (prev.includes(i) ? prev : [...prev, i]))
}, [])
const removeBusy = useCallback((i: number) => {
setBusyIndexes((prev) => prev.filter((x) => x !== i))
}, [])
const addUploading = useCallback((i: number) => {
setUploadingIndexes((prev) => (prev.includes(i) ? prev : [...prev, i]))
}, [])
const removeUploading = useCallback((i: number) => {
setUploadingIndexes((prev) => prev.filter((x) => x !== i))
}, [])
/** 函数式回写单个封面位置(基于最新 prev,不受闭包旧值影响) */
const patchCover = useCallback(
(index: number, url: string) => {
const next = [...covers]
next[index] = url
onCoversChange(next)
onCoversChange((prev) => {
const next = [...prev]
next[index] = url
return next
})
},
[covers, onCoversChange],
[onCoversChange],
)
/** 为第 index 个视频自动生成封面 */
/** 为第 index 个视频自动生成封面;返回是否成功(供 generateAll 统计) */
const generateOne = useCallback(
async (index: number) => {
async (index: number): Promise<boolean> => {
const finalVideos = generatedVideos.filter((v) => v.status === "completed")
const target = finalVideos[index] || generatedVideos[index]
if (!target) {
message.warning("该视频尚未生成完成")
return
return false
}
setLoadingIndex(index)
addBusy(index)
try {
const titleText = titles[index] || ""
const response = await generateCover(selectedTemplate, {
@@ -82,23 +110,25 @@ export function useBatchCovers({
if (url) {
patchCover(index, url)
message.success(`视频 ${index + 1} 封面生成成功`)
} else {
message.warning(`视频 ${index + 1} 封面生成未返回图片,请重试`)
return true
}
message.warning(`视频 ${index + 1} 封面生成未返回图片,请重试`)
return false
} catch (err) {
console.error(`[封面] 视频 ${index + 1} 生成失败:`, err)
message.error(`视频 ${index + 1} 封面生成失败,请重试`)
return false
} finally {
setLoadingIndex(null)
removeBusy(index)
}
},
[generatedVideos, titles, titleStyle, selectedTemplate, patchCover],
[generatedVideos, titles, titleStyle, selectedTemplate, patchCover, addBusy, removeBusy],
)
/** 为第 index 个视频上传自定义封面 */
const uploadOne = useCallback(
async (index: number, file: File) => {
setUploadingIndex(index)
addUploading(index)
try {
const libs = await getAssetLibraries()
const imageLib = libs.find((l) => l.kind === "image") || libs[0]
@@ -121,26 +151,43 @@ export function useBatchCovers({
console.error(`[封面] 视频 ${index + 1} 上传失败:`, err)
message.error("封面上传失败,请重试")
} finally {
setUploadingIndex(null)
removeUploading(index)
}
},
[patchCover],
[patchCover, addUploading, removeUploading],
)
/** 一键全部自动生成(串行,避免队列限流) */
/** 一键全部自动生成(串行,避免队列限流;单个失败不阻塞,结束后分级提示) */
const generateAll = useCallback(async () => {
const finalVideos = generatedVideos.filter((v) => v.status === "completed")
for (let i = 0; i < finalVideos.length; i++) {
if (covers[i]) continue // 已有封面跳过
const total = finalVideos.length
// 待处理:基于调用时刻的 covers 快照判断(已有封面跳过);
// 回写走函数式 updater,循环内不再依赖可能过期的 covers 闭包
const pending = finalVideos.map((_, i) => i).filter((i) => !covers[i])
if (pending.length === 0) {
message.info("所有视频封面已生成")
return
}
let okCount = 0
let failCount = 0
for (const i of pending) {
// eslint-disable-next-line no-await-in-loop
await generateOne(i)
const ok = await generateOne(i)
if (ok) okCount += 1
else failCount += 1
}
if (failCount === 0) {
message.success(`全部 ${total} 个封面已生成`)
} else if (okCount === 0) {
message.error("封面生成全部失败,请重试")
} else {
message.warning(`已生成 ${okCount} 个封面,${failCount} 个失败,请对失败项重试`)
}
message.success("全部封面已生成")
}, [generatedVideos, covers, generateOne])
return {
loadingIndex,
uploadingIndex,
busyIndexes,
uploadingIndexes,
generateOne,
uploadOne,
generateAll,
@@ -0,0 +1,153 @@
/**
* 批量变体真实片段 Hook(#1744 接入 / #1750 修正)
*
* 批量预览(N>1)进入第 4 步时,向后端轻量接口 POST /generation/variant-plans
* 申请 N 个变体的独立剪辑计划片段:
* - 接口只做选片/建 plan(秒级),不渲染视频,无渲染成本;
* - 选片逻辑与正式批量生成(POST /generation/tasks?count=N)完全同源
* (reselect_plan_for_variant:素材洗牌 + main 片段洗牌 + 起点随机 +
* 跨变体 20% 区间避让 + 使用区间写回素材 metadata),
* 因此前端按这些 clips 播放的预览与最终成片一致;
* - 正式生成时把 plan_ids 回传,后端直接关联预览 plan 渲染,不再重新选片。
*
* 错误处理(#1750 基线:严禁用前端假数据冒充预览):
* - 端点 404(后端未上线)/ 400 / 网络错误 / 超时:一律置 error 状态,
* UI 显示「预览加载失败,请重试」,不再静默降级本地模拟;
* - 单个变体 clips 为空同样视为加载失败(计划数据不完整)。
*
* N=1 不调用本 hook(单视频零回归)。
*/
import { useCallback, useEffect, useRef, useState } from "react"
import type { EditPlanClip } from "@/api/template-editor"
import { createBatchVariantPlans, type VariantPlan } from "@/api/generation/variantPlans"
export interface BatchVariantClipsState {
/** 各变体的服务端真实片段(按 variant_index 排序);未就绪为空数组 */
clipsByVariant: EditPlanClip[][]
/** 各变体的 plan_id(正式生成回传,保证预览即成片);未就绪为空串 */
planIdsByVariant: string[]
/** 是否正在向后端申请变体计划 */
loading: boolean
/** 后端真实片段是否全部可用(每个变体都有 ≥1 条片段) */
ready: boolean
/** 加载失败(端点未上线/网络错误/数据不完整);true 时 UI 显示错误态+重试 */
error: boolean
/** 用户点击「重试」:强制重新申请变体计划 */
retry: () => void
}
interface UseBatchVariantPlansOptions {
/** 是否启用:仅批量(count>1)且素材已选时为 true */
enabled: boolean
count: number
templateId: string
assetIds: string[]
/** 源剪辑计划 ID(草稿/预览关联),无则空串由后端兜底最新 plan */
sourcePlanId?: string
}
export function useBatchVariantPlans({
enabled,
count,
templateId,
assetIds,
sourcePlanId = "",
}: UseBatchVariantPlansOptions): BatchVariantClipsState {
const [clipsByVariant, setClipsByVariant] = useState<EditPlanClip[][]>([])
const [planIdsByVariant, setPlanIdsByVariant] = useState<string[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const requestSeqRef = useRef(0)
// 记录上次成功申请的入参指纹,素材/数量未变时不重复请求
const lastKeyRef = useRef("")
// 重试计数(state):用户点重试时 +1,驱动 effect 重新发起请求(即使指纹相同);
// 用 state 而非 ref + 直接调 load,避免 retry 与 effect 双发导致 seq 竞争
const [retryNonce, setRetryNonce] = useState(0)
const load = useCallback(async () => {
const seq = ++requestSeqRef.current
setLoading(true)
setError(false)
try {
const resp = await createBatchVariantPlans({
template_id: templateId,
asset_ids: assetIds,
count,
...(sourcePlanId ? { source_edit_plan_id: sourcePlanId } : {}),
})
if (seq !== requestSeqRef.current) return
const items: VariantPlan[] = Array.isArray(resp.items) ? resp.items : []
const clips: EditPlanClip[][] = Array.from({ length: count }, () => [])
const planIds: string[] = Array.from({ length: count }, () => "")
for (const item of items) {
const idx = item.variant_index
if (idx < 0 || idx >= count) continue
planIds[idx] = item.plan_id || ""
clips[idx] = (item.clips || [])
.filter((c) => c && c.asset_id && c.status === "ready")
.sort((a, b) => a.order - b.order)
}
// 数据完整性校验:每个变体都必须有真实片段,否则视为失败(不允许假数据冒充)
const incomplete = clips.some((list) => list.length === 0)
if (incomplete) {
console.warn("[useBatchVariantPlans] 变体计划数据不完整(存在空片段变体),标记加载失败")
setClipsByVariant([])
setPlanIdsByVariant([])
setError(true)
} else {
setClipsByVariant(clips)
setPlanIdsByVariant(planIds)
setError(false)
}
} catch (err) {
if (seq !== requestSeqRef.current) return
// 404(后端端点未上线)/ 400(参数错误)/ 网络错误 / 超时:统一错误态
console.warn("[useBatchVariantPlans] 申请变体计划失败,预览加载失败:", err)
setClipsByVariant([])
setPlanIdsByVariant([])
setError(true)
} finally {
if (seq === requestSeqRef.current) setLoading(false)
}
}, [templateId, count, sourcePlanId, assetIds])
/** 用户点击「重试」:nonce +1 驱动 effect 重新发起请求(effect 内 lastKey 校验保证只发一次) */
const retry = useCallback(() => {
setRetryNonce((n) => n + 1)
}, [])
useEffect(() => {
if (!enabled || !templateId || assetIds.length === 0 || count <= 1) {
requestSeqRef.current += 1
// 函数式更新:已是目标值时返回 prev(Object.is 相等 React 跳过渲染),
// 避免父组件传入内联字面量数组导致 effect 每次 render 触发 → 无限 setState 循环
setClipsByVariant((prev) => (prev.length === 0 ? prev : []))
setPlanIdsByVariant((prev) => (prev.length === 0 ? prev : []))
setLoading((prev) => (prev === false ? prev : false))
setError((prev) => (prev === false ? prev : false))
lastKeyRef.current = ""
return
}
const key = `${retryNonce}|${templateId}|${count}|${sourcePlanId}|${[...assetIds]
.sort()
.join(",")}`
if (key === lastKeyRef.current) return
lastKeyRef.current = key
load()
}, [enabled, templateId, count, sourcePlanId, assetIds, load, retryNonce])
const ready = !error && !loading && clipsByVariant.every((list) => list.length > 0)
return {
clipsByVariant,
planIdsByVariant,
loading,
ready,
error,
retry,
}
}
export default useBatchVariantPlans
@@ -40,6 +40,8 @@ export interface GenerateFormState {
selectedTemplate: string
setSelectedTemplate: (id: string) => void
userTemplates: EditingTemplate[]
/** 当前选中模板在创建片段时被判失效(404/400/422)后的运行时自动回退 */
handleInvalidTemplate: () => boolean
/* 素材 */
selectedMaterials: string[]
@@ -131,7 +133,8 @@ export const useGenerateFormState = (): GenerateFormState => {
const [currentStep, setCurrentStep] = useState(1)
/* ── 模板选择 ── */
const { selectedTemplate, setSelectedTemplate, userTemplates } = useTemplateSelection()
const { selectedTemplate, setSelectedTemplate, userTemplates, handleInvalidTemplate } =
useTemplateSelection()
/* ── source_edit_plan_id:仅取 URL 参数,无则 null 让后端兜底 ── */
// selectedTemplate 是模板 ID 而非 edit_plan_id,不能混淆;
@@ -228,6 +231,7 @@ export const useGenerateFormState = (): GenerateFormState => {
selectedTemplate,
setSelectedTemplate,
userTemplates,
handleInvalidTemplate,
selectedMaterials,
setSelectedMaterials,
materialMode,
@@ -0,0 +1,81 @@
/**
* 失效模板判定与自动回退工具(#1777)
*
* 背景:用户进入生成页后,之前选中的模板可能已被删除、或从未配置片段。
* 调用片段相关接口(PUT/POST /templates/{id}/editor/clips[...]/from-assets)时:
* - 模板不存在 → 后端返回 404(并行工单 #1774 把「模板不存在」统一为该状态码)
* - 模板无片段配置 → 当前部分场景返回 400(detail 含「片段配置」),
* 参数校验类错误返回 422
* 这三类响应都说明「当前选中的模板不可用于生成」,应清除失效选择并自动切换到
* 第一个有效模板,同时提示用户,而不是让页面卡死、无任何反馈。
*/
import type { EditingTemplate } from "@/api/editing-planner"
/** 失效模板相关的 HTTP 状态码 */
const INVALID_TEMPLATE_STATUSES = new Set([404, 400, 422])
/**
* 从任意抛出值(axios 错误)提取 HTTP 状态码。
* 非 axios 错误 / 无响应时返回 null。
*/
export function getHttpStatus(err: unknown): number | null {
if (!err || typeof err !== "object") return null
const status = (err as { response?: { status?: number }; status?: number })?.response?.status
return typeof status === "number" ? status : null
}
/** 安全提取后端错误文本(detail/message/msg,422 数组也兜底拼一下) */
function extractErrorText(err: unknown): string {
if (!err || typeof err !== "object") return ""
const data = (err as { response?: { data?: unknown } })?.response?.data
if (!data) return ""
try {
const text = JSON.stringify(data)
return typeof text === "string" ? text : ""
} catch {
return ""
}
}
/**
* 判断一次 clips/from-assets 请求失败是否因为「模板失效」。
*
* 严格判定,避免把无关的 400/422(例如素材参数问题)误判为模板失效:
* - 404:模板/编辑计划不存在,一定是模板失效
* - 400:仅当后端文本明确提到「片段配置」(无片段配置无法创建片段)才判定
* - 422:参数校验类,from-assets 场景下命中「片段/segments」相关字段才判定
*/
export function isInvalidTemplateError(err: unknown): boolean {
const status = getHttpStatus(err)
if (status === null || !INVALID_TEMPLATE_STATUSES.has(status)) return false
if (status === 404) return true
const text = extractErrorText(err)
if (status === 400) {
// 后端当前返回:「模板没有片段配置,无法创建片段」
return /片段配置|没有片段|无片段|segments?|clip.*config/i.test(text)
}
// 422:FastAPI 校验错误,命中模板片段相关字段
return /segment|clip|片段|模板/i.test(text)
}
/**
* 判断模板是否可用于生成(有效模板)。
*
* 有效 = 处于激活态(is_active !== false,字段缺失视为 true 兼容旧后端)
* 且至少配置了一个片段。
* 与后端 valid_only 过滤口径保持一致(#1769/#1772),这里是前端双保险。
*/
export function isValidTemplate(template: EditingTemplate | null | undefined): boolean {
if (!template) return false
if (template.is_active === false) return false
return (template.segments?.length ?? 0) > 0
}
/** 从模板列表中取出第一个有效模板,没有则返回 null */
export function findFirstValidTemplate(
templates: EditingTemplate[] | null | undefined,
): EditingTemplate | null {
if (!Array.isArray(templates)) return null
return templates.find(isValidTemplate) ?? null
}
@@ -1,22 +1,89 @@
import { useState, useEffect } from "react"
import { useState, useEffect, useRef, useCallback, useMemo } from "react"
import { useQuery } from "@tanstack/react-query"
import { message } from "antd"
import { getEditingTemplates } from "@/api/editing-planner"
import type { EditingTemplate } from "@/api/editing-planner"
import { findFirstValidTemplate, isValidTemplate } from "./templateFallback"
/** 失效模板自动切换的提示文案 */
export const INVALID_TEMPLATE_FALLBACK_TOAST = "原模板已失效,已自动切换"
export function useTemplateSelection() {
// selectedTemplate 纯内存状态,绝不写入 localStorage/sessionStorage/URL,
// 因此失效模板 ID 不会被持久化、刷新后也不会恢复(#1777 要求 4)
const [selectedTemplate, setSelectedTemplate] = useState("")
const { data: userTemplates = [] } = useQuery<EditingTemplate[]>({
const { data: allTemplates = [] } = useQuery<EditingTemplate[]>({
queryKey: ["generate-templates"],
queryFn: () => getEditingTemplates(),
// valid_only:后端过滤掉没有片段配置的无效模板(#1769/#1772)。
// 旧后端忽略该 query 参数时,下方 isValidTemplate 前端兜底再过滤一次。
queryFn: () => getEditingTemplates({ validOnly: true }),
staleTime: 60_000,
})
/* 模板加载完成后自动选中第一个 */
useEffect(() => {
if (userTemplates.length > 0 && !selectedTemplate) {
setSelectedTemplate(userTemplates[0].id)
}
}, [userTemplates, selectedTemplate])
// 双保险:后端 valid_only 已过滤,前端再按 is_active + segments 兜底,
// 保证下拉/自动选择只包含可用于生成的有效模板。
// 用 useMemo 缓存引用,避免每次渲染都 .filter 创建新数组,
// 导致下游 useTitleCoverSync effect 无限触发、覆盖用户手动修改(#1789)
const validTemplates = useMemo(() => allTemplates.filter(isValidTemplate), [allTemplates])
const userTemplates = validTemplates
return { selectedTemplate, setSelectedTemplate, userTemplates }
// 用 ref 持有最新值,供稳定回调 handleInvalidTemplate 使用(避免闭包拿到旧值)
const templatesRef = useRef(validTemplates)
templatesRef.current = validTemplates
const selectedRef = useRef(selectedTemplate)
selectedRef.current = selectedTemplate
// 已提示过失效的模板 ID,避免用户停留在失效模板上时 clips 防抖请求反复弹 toast;
// 用户手动切换/成功切换后重置,保证下一个失效模板仍能提示
const fallbackNotifiedRef = useRef<string>("")
/* 自动选择:模板加载完成且当前未选中时,自动选中第一个有效模板。
* 用户手动选择(setSelectedTemplate 被显式调用)后 selectedTemplate 非空,
* 本 effect 直接 return,绝不覆盖用户的手动选择(#1777 要求 4:手动优先)。 */
useEffect(() => {
if (selectedTemplate) return
const firstValid = validTemplates[0]
if (firstValid) {
setSelectedTemplate(firstValid.id)
}
}, [validTemplates, selectedTemplate])
/** 用户手动选择模板:优先级最高,重置失效提示标记 */
const handleSelectTemplate = useCallback((id: string) => {
fallbackNotifiedRef.current = ""
setSelectedTemplate(id)
}, [])
/**
* 运行时失效回退(#1777 要求 3):
* 创建片段接口返回 404(模板不存在)/ 400/422(模板无片段配置)时调用。
* - 清除失效选择,自动切换到第一个有效模板,并 toast 提示;
* - 没有有效模板时清空选择,Step1 展示明确的「暂无可用模板」空状态引导,
* 不让用户卡在失效模板上。
* 返回 true 表示已按「模板失效」处理(调用方可据此静默原始错误提示)。
*/
const handleInvalidTemplate = useCallback((): boolean => {
const current = selectedRef.current
// 同一个失效模板只提示一次(clips 防抖 effect 在素材/模板变化时会反复触发)
if (current && fallbackNotifiedRef.current === current) return true
const fallback = findFirstValidTemplate(templatesRef.current)
fallbackNotifiedRef.current = current || "__empty__"
if (fallback) {
setSelectedTemplate(fallback.id)
message.warning(INVALID_TEMPLATE_FALLBACK_TOAST)
} else {
// 没有任何有效模板:清空选择,交由 Step1 空状态引导用户去模板编辑器创建
setSelectedTemplate("")
message.warning("当前没有可用模板,请先在「模板编辑器」中创建并配置片段")
}
return true
}, [])
return {
selectedTemplate,
setSelectedTemplate: handleSelectTemplate,
userTemplates,
handleInvalidTemplate,
}
}
@@ -1,4 +1,4 @@
import { useEffect } from "react"
import { useEffect, useRef } from "react"
import type { TitleSettings } from "../../types"
import type { CoverConfig } from "../../types/cover"
import type { EditingTemplate } from "@/api/editing-planner"
@@ -11,7 +11,11 @@ interface UseTitleCoverSyncOptions {
}
/**
* 当选中模板变化时,自动同步标题和封面配置
* 当选中模板变化时,自动同步标题和封面配置。
*
* #1789 修复:userTemplates 用 ref 持有最新值,不放入依赖数组。
* 否则每次渲染 .filter() 创建的新数组引用都会触发 effect,
* 从模板 title_config 覆盖用户手动修改(如字号滑块拖动),导致回弹。
*/
export function useTitleCoverSync({
selectedTemplate,
@@ -19,8 +23,12 @@ export function useTitleCoverSync({
setTitleSettings,
setCoverSettings,
}: UseTitleCoverSyncOptions) {
// 用 ref 持有最新 userTemplates,避免数组引用变化导致 effect 反复触发
const templatesRef = useRef(userTemplates)
templatesRef.current = userTemplates
useEffect(() => {
const tpl = userTemplates.find((t) => t.id === selectedTemplate)
const tpl = templatesRef.current.find((t) => t.id === selectedTemplate)
if (tpl?.title_config) {
setTitleSettings((prev: TitleSettings) => ({
...prev,
@@ -43,5 +51,7 @@ export function useTitleCoverSync({
thumbnail_url: tpl.cover_config!.thumbnail_url || prev.thumbnail_url,
}))
}
}, [selectedTemplate, userTemplates, setTitleSettings, setCoverSettings])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTemplate, setTitleSettings, setCoverSettings])
// ↑ 移除 userTemplates,只在 selectedTemplate 真正变化时触发
}
@@ -178,6 +178,14 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
isBatch && props.variantCoverUrls?.length
? indexes.map((i) => props.variantCoverUrls![i] || "")
: []
// #1744 变体 plan 数组:预览阶段后端独立选片产出的 plan id,按勾选顺序回传,
// 后端直接关联这些 plan 渲染(不再重新选片)→ 预览所见即成片。
// 全部为空(降级本地模拟/后端端点未上线)时不传,后端走自身独立选片。
const variantPlansArr =
isBatch && props.variantPlanIds?.length
? indexes.map((i) => props.variantPlanIds![i] || "")
: []
const hasVariantPlans = variantPlansArr.some((id) => !!id)
try {
const taskResp = await createGenerationTask({
@@ -200,6 +208,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
...(titlesArr.length ? { titles: titlesArr } : {}),
...(voiceArr.length ? { voice_library_ids: voiceArr } : {}),
...(coversArr.length ? { cover_urls: coversArr } : {}),
...(hasVariantPlans ? { variant_plan_ids: variantPlansArr } : {}),
...(props.titleSettings?.title
? {
title_config: {
@@ -44,6 +44,12 @@ interface UsePreviewAssetsReturn {
ready: boolean
/** 手动触发重新加载 */
reload: () => void
/**
* 差集补拉(#1750):后端变体计划 clips 可能引用不在用户已选列表中的素材
* (跨素材库选片/素材池扩展),发现 assets 中缺失的 asset_id 时补拉详情并合并,
* 保证预览播放器拿得到素材文件 URL,而不是静默丢片段。
*/
ensureAssets: (ids: string[]) => Promise<void>
}
/**
@@ -91,11 +97,41 @@ export function usePreviewAssets(assetIds: string[], enabled: boolean): UsePrevi
}
}, [stableAssetIds, enabled])
// 补拉用独立序号:不干扰主 load 的竞态守卫
const ensureSeqRef = useRef(0)
const assetsRef = useRef<AssetItem[]>([])
useEffect(() => {
assetsRef.current = assets
}, [assets])
/**
* 差集补拉(#1750):变体计划 clips 引用的 asset_id 不在当前素材列表时,
* 补拉素材详情并去重合并(不静默丢片段、不用假数据冒充)。
*/
const ensureAssets = useCallback(async (ids: string[]) => {
const validIds = ids.filter((id): id is string => typeof id === "string" && id.length > 0)
if (!validIds.length) return
const seq = ++ensureSeqRef.current
const missing = Array.from(new Set(validIds)).filter(
(id) => !assetsRef.current.some((a) => a.id === id),
)
if (!missing.length) return
const fetched = await fetchAssetsByIds(missing)
if (seq !== ensureSeqRef.current || !fetched.length) return
const existing = new Set(assetsRef.current.map((a) => a.id))
const additions = fetched.filter((a) => !existing.has(a.id))
if (!additions.length) return
const merged = [...assetsRef.current, ...additions]
assetsRef.current = merged
setAssets(merged)
setReady(merged.length > 0)
}, [])
useEffect(() => {
load()
}, [load])
return { assets, loading, ready, reload: load }
return { assets, loading, ready, reload: load, ensureAssets }
}
/**
@@ -15,6 +15,8 @@ export interface PlaybackSegment {
startTime: number
endTime: number
order: number
/** #1754 兜底:配音时长≠clips 总时长时按比例调速,1.0 = 原速 */
playbackRate?: number
}
export interface SegmentSchedulerState {
@@ -131,6 +133,11 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
if (Math.abs(video.currentTime - localTime) > 0.05) {
video.currentTime = localTime
}
// #1754:按比例调速(配音时长≠clips 总时长时的前端兜底)
const rate = seg.playbackRate || 1
if (Math.abs(video.playbackRate - rate) > 0.01) {
video.playbackRate = rate
}
segIdxRef.current = index
setCurrentSegmentIndex(index)
@@ -242,6 +249,12 @@ export function useSegmentScheduler(segments: PlaybackSegment[]): SegmentSchedul
video.currentTime = seg.startTime
}
// #1754:调速
const rate = seg?.playbackRate || 1
if (Math.abs(video.playbackRate - rate) > 0.01) {
video.playbackRate = rate
}
try {
await video.play()
setIsPlaying(true)
@@ -1,297 +0,0 @@
/**
* 服务器渲染预览 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
// 兼容批量响应 {items, total}:取第一个变体
const firstTask = resp.items?.[0]
const taskId = firstTask?.task_id || ""
setTaskId(taskId)
onCreatedRef.current?.(taskId, 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(taskId)
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
@@ -10,6 +10,7 @@ import { updateEditPlanClips, createClipsFromAssets, getEditPlanClips } from "@/
import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary"
import { useSmartMatch } from "./step2-materials/useSmartMatch"
import { useDraftAutoSave } from "./useDraftAutoSave"
import { isInvalidTemplateError } from "./useGenerateFormState/templateFallback"
interface UseStep2MaterialsProps {
materialMode: "manual" | "auto"
@@ -24,6 +25,8 @@ interface UseStep2MaterialsProps {
templateSegments?: TemplateSegment[]
/** 服务端 clips 创建成功后的回调,用于通知预览播放器 */
onServerClipsChange?: (clips: EditPlanClip[]) => void
/** 当前模板创建片段返回 404/400/422(模板失效)时的自动回退回调(#1777) */
onTemplateInvalid?: () => boolean
}
export function useStep2Materials({
@@ -36,6 +39,7 @@ export function useStep2Materials({
selectedTemplate,
templateSegments,
onServerClipsChange,
onTemplateInvalid,
}: UseStep2MaterialsProps) {
const {
libraries,
@@ -102,6 +106,8 @@ export function useStep2Materials({
selectedTemplateRef.current = selectedTemplate
const onServerClipsChangeRef = useRef(onServerClipsChange)
onServerClipsChangeRef.current = onServerClipsChange
const onTemplateInvalidRef = useRef(onTemplateInvalid)
onTemplateInvalidRef.current = onTemplateInvalid
useEffect(() => {
const tid = selectedTemplateRef.current
@@ -123,11 +129,12 @@ export function useStep2Materials({
const requiredClipsCount = segs.length > 0 ? segs.length : undefined
try {
// 1. 清空旧片段
await updateEditPlanClips(tid, [], controller.signal)
// 1. 清空旧片段(静默全局 toast:模板失效时由下方回退统一提示)
await updateEditPlanClips(tid, [], controller.signal, true)
// 2. 调用后端 from-assets 接口创建片段(异步秒级返回,60s 超时仅为兜底)
await createClipsFromAssets(tid, ids, "main", requiredClipsCount, {
signal: controller.signal,
silentErrorToast: true,
})
// 3. 获取服务端生成的 clips(含 start_time/duration),供预览播放器使用
const clipList = await getEditPlanClips(tid, { limit: 500 })
@@ -146,6 +153,14 @@ export function useStep2Materials({
message.error("智能选片失败,请重试")
return
}
// 模板失效(404 模板不存在 / 400/422 无片段配置):
// 清空失效选择并自动切到第一个有效模板 + toast,避免页面卡死无提示(#1777)
if (isInvalidTemplateError(err)) {
console.warn("[useStep2Materials] 当前模板已失效,触发自动回退:", err)
onServerClipsChangeRef.current?.([])
onTemplateInvalidRef.current?.()
return
}
console.warn("[useStep2Materials] 写入 clips 失败:", err)
}
}, 800)
@@ -0,0 +1,166 @@
/**
* 批量变体配音预览音频解析(#1750)
*
* 独立配音模式下每个变体挂载各自的配音 URL:
* - 配音素材(voice 资产,有 file_url)→ 直接用素材文件 URL;
* - AI 音色(预置/克隆,无实体文件)→ 按该变体自己的标题调 /tts/preview 合成;
* - 共用模式下所有变体解析为同一条(等价于旧 previewVoiceAudioUrl)。
*
* N=1 不使用本 hook(单视频配音预览逻辑在 GeneratePage 内保持不变,零回归)。
*/
import { useEffect, useRef, useState } from "react"
import { useQuery } from "@tanstack/react-query"
import { getAssetsByKind } from "@/api/assets"
import type { AssetItem } from "@/api/assets"
import { previewTts } from "@/api/tts"
interface UseVariantVoicePreviewOptions {
/** 是否批量模式(count>1) */
enabled: boolean
/** 变体数量 */
count: number
/** 是否每个视频独立配音 */
perVideo: boolean
/** 共用配音 ID(独立模式下为变体 0 的配音) */
sharedVoiceId: string
/** 克隆音色 ID 覆盖(共用模式,与旧逻辑一致:selectedClonedVoice || selectedVoice) */
clonedVoiceId?: string
/** 各变体独立配音 ID(独立模式);长度不足时回退共用 */
variantVoiceIds: string[]
/** 各变体标题(TTS 合成文案源) */
titles: string[]
}
/** 稳定的空数组常量:避免 useQuery 数据未就绪时每次渲染产生新引用导致 effect 无限触发 */
const EMPTY_VOICE_MATERIALS: AssetItem[] = []
/** 判断配音 ID 是否对应实体素材(有 file_url);否则视为 AI 音色需 TTS */
function findMaterialUrl(id: string, materials: AssetItem[]): string | null {
if (!id) return null
const m = materials.find((x) => x.id === id)
return m?.file_url || null
}
export function useVariantVoicePreview({
enabled,
count,
perVideo,
sharedVoiceId,
clonedVoiceId = "",
variantVoiceIds,
titles,
}: UseVariantVoicePreviewOptions): (string | null)[] {
const [urls, setUrls] = useState<(string | null)[]>([])
// 配音素材库:组件内部自取,避免调用方传入不稳定数组引用导致 effect 反复触发
const { data: voiceMaterialsData } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
})
const voiceMaterials: AssetItem[] = voiceMaterialsData ?? EMPTY_VOICE_MATERIALS
// 已缓存的 TTS 结果:key = `${voiceId}|${title}`,避免重复合成
const ttsCacheRef = useRef<Map<string, string>>(new Map())
// 在途请求 AbortController
const abortRef = useRef<AbortController | null>(null)
const seqRef = useRef(0)
useEffect(() => {
if (!enabled || count <= 1) {
setUrls((prev) => (prev.length === 0 ? prev : []))
return
}
const seq = ++seqRef.current
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
// 每个变体实际使用的配音 ID
const voiceIds = Array.from({ length: count }, (_, i) =>
perVideo ? variantVoiceIds[i] || sharedVoiceId : sharedVoiceId,
)
// 先用素材 URL 同步填充;AI 音色位置先置 null,待 TTS 完成
const result: (string | null)[] = voiceIds.map((id, i) => {
const materialUrl = findMaterialUrl(id, voiceMaterials)
if (materialUrl) return materialUrl
// 共用模式下克隆音色 ID 可能与 selectedVoice 不同(与旧 useEffect 逻辑一致)
if (!perVideo && i === 0 && clonedVoiceId) {
return findMaterialUrl(clonedVoiceId, voiceMaterials)
}
return null
})
// 收集需要 TTS 的变体:无素材 URL 且有标题且有音色 ID
const ttsJobs: { index: number; voiceId: string; title: string }[] = []
voiceIds.forEach((id, i) => {
if (result[i]) return
// 共用模式沿用旧逻辑:voice_id = selectedClonedVoice || selectedVoice
const ttsVoiceId = !perVideo && i === 0 ? clonedVoiceId || id : id
const title = titles[i] || ""
if (!ttsVoiceId || !title) return
ttsJobs.push({ index: i, voiceId: ttsVoiceId, title })
})
setUrls((prev) =>
prev.length === result.length && prev.every((v, i) => v === result[i]) ? prev : result,
)
if (ttsJobs.length === 0) return
let cancelled = false
void (async () => {
// 串行 TTS,避免瞬间 N 个合成请求打爆后端
for (const job of ttsJobs) {
const cacheKey = `${job.voiceId}|${job.title}`
const cached = ttsCacheRef.current.get(cacheKey)
if (cached) {
if (seq === seqRef.current) {
setUrls((prev) => {
if (prev[job.index] === cached) return prev
const next = [...prev]
next[job.index] = cached
return next
})
}
continue
}
try {
// eslint-disable-next-line no-await-in-loop
const res = await previewTts({ text: job.title, voice_id: job.voiceId })
if (cancelled || controller.signal.aborted || seq !== seqRef.current) return
const audioUrl = res.audio_url || ""
if (audioUrl) {
ttsCacheRef.current.set(cacheKey, audioUrl)
setUrls((prev) => {
if (prev[job.index] === audioUrl) return prev
const next = [...prev]
next[job.index] = audioUrl
return next
})
}
} catch (err) {
if (cancelled || controller.signal.aborted || seq !== seqRef.current) return
console.warn(`[变体${job.index + 1}预览配音生成失败]`, err)
}
}
})()
return () => {
cancelled = true
controller.abort()
}
}, [
enabled,
count,
perVideo,
sharedVoiceId,
clonedVoiceId,
variantVoiceIds,
titles,
voiceMaterials,
])
return urls
}
export default useVariantVoicePreview
@@ -1,54 +0,0 @@
/**
* 共享:根据素材列表和模板片段计算总视频时长
* GeneratePage(配音校验)和 FrontendPreviewPlayer(播放控制)共用
*/
export interface DurationAsset {
id?: string
duration?: number
metadata?: { duration?: number }
}
export interface DurationTemplateSegment {
duration_min?: number
duration_max?: number
}
/**
* 计算总视频时长
* @param assets 素材列表
* @param template 模板(含 segments)
* @returns 总时长(秒),无有效数据时返回 0
*/
export function calculateTotalVideoDuration(
assets: DurationAsset[] | undefined,
template: { segments?: DurationTemplateSegment[] } | undefined,
): number {
if (!assets || assets.length === 0 || !template) return 0
const templateSegments = template.segments || []
return assets.reduce((sum, asset, i) => {
const assetDuration = asset.duration || asset.metadata?.duration || 30
const tplSeg = templateSegments[i] || templateSegments[templateSegments.length - 1]
const segDuration = tplSeg
? Math.min(
tplSeg.duration_max ?? assetDuration,
Math.max(tplSeg.duration_min ?? 0, assetDuration),
)
: Math.min(assetDuration, 10)
return sum + segDuration
}, 0)
}
/**
* 估算总视频时长(仅依赖模板 segments)
* 当素材未加载或加载失败时,用各片段 duration_max 之和作为估算值
* 确保配音时长校验不会因素材未就绪而跳过
*/
export function estimateTotalVideoDuration(
template: { segments?: DurationTemplateSegment[] } | undefined,
): number {
if (!template?.segments || template.segments.length === 0) return 0
return template.segments.reduce((sum, seg) => sum + (seg.duration_max || 0), 0)
}
@@ -66,7 +66,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
<div className="mt-card-meta">
<Text type="secondary" style={{ fontSize: 12 }}>
{tpl.segments.length} 片段 · 预估 ~{tpl.estimated_duration}s
{tpl.segments.length} 个片段
</Text>
{tpl.category && <Tag style={{ fontSize: 11, marginTop: 4 }}>{tpl.category}</Tag>}
</div>
@@ -57,11 +57,6 @@ export function useMyTemplates() {
title_config: tpl.title_config,
subtitle_config: tpl.subtitle_config,
bgm_config: tpl.bgm_config,
estimated_duration:
tpl.estimated_duration ??
Math.round(
tpl.segments.reduce((s, seg) => s + (seg.duration_min + seg.duration_max) / 2, 0),
),
segments: tpl.segments.map(({ id: _id, ...rest }) => rest),
}),
onSuccess: () => {
@@ -1,7 +1,7 @@
/**
* 模板库页面(升级版)— V21 设计系统
* 对接后端模板管理 API:
* - 分页查询(page/page_size/category/keyword/duration_range)
* - 分页查询(page/page_size/category/keyword)
* - 模板详情(素材规则、字幕样式、BGM、比例等参数配置)
* - 复制模板 / 从模板生成
* - 卡片网格布局 + 类型筛选 + 搜索 + 收藏
@@ -24,7 +24,6 @@ const TemplateLibrary: React.FC = () => {
error,
searchText,
activeType,
durationRange,
page,
pageSize,
setPage,
@@ -34,7 +33,6 @@ const TemplateLibrary: React.FC = () => {
handleCreate,
handleSearchChange,
handleCategoryChange,
handleDurationChange,
} = useTemplateLibrary()
const {
@@ -62,8 +60,6 @@ const TemplateLibrary: React.FC = () => {
onSearchChange={handleSearchChange}
activeType={activeType}
onTypeChange={handleCategoryChange}
durationRange={durationRange}
onDurationChange={handleDurationChange}
/>
{/* 模板展示区 */}
@@ -77,7 +73,6 @@ const TemplateLibrary: React.FC = () => {
errorMessage={error?.message}
searchText={searchText}
activeType={activeType}
durationRange={durationRange}
onPageChange={setPage}
onPreview={handlePreview}
onToggleFavorite={toggleFavorite}
@@ -1,7 +1,7 @@
import React from "react"
import { Tag } from "antd"
import type { TemplateItem } from "@/api/templates"
import { gradientForCategory, getTypeColor, formatDuration } from "../../utils/templateLibrary"
import { gradientForCategory, getTypeColor } from "../../utils/templateLibrary"
interface TemplateCardProps {
template: TemplateItem
@@ -37,7 +37,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({
<div className="xx-template-thumb-name">{template.name}</div>
<div className="xx-template-thumb-meta">
<span className="xx-template-thumb-duration">
{formatDuration(template.estimated_duration ?? template.target_duration)}
{template.clip_count || template.segments?.length || 0} 个片段
</span>
</div>
<div className="xx-template-preview-hint">点击查看详情</div>
@@ -5,9 +5,8 @@ import PreviewArea from "./template-detail-modal/PreviewArea"
import SegmentList from "./template-detail-modal/SegmentList"
import StyleConfig from "./template-detail-modal/StyleConfig"
import DetailFooter from "./template-detail-modal/DetailFooter"
import { getTypeColor, formatDuration } from "../../utils/templateLibrary"
import { getTypeColor } from "../../utils/templateLibrary"
import { TEMPLATE_TYPES } from "../../constants/templateLibrary"
import { calcTotalSegmentDuration } from "../../utils/templateLibrary"
export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
template,
@@ -18,7 +17,6 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
onCopy,
}) => {
const segments = template.segments ?? []
const totalSegmentDuration = calcTotalSegmentDuration(segments)
const typeInfo = TEMPLATE_TYPES.find((t) => t.type === template.category)
return (
@@ -64,11 +62,6 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
size="small"
className="xx-template-modal-desc-table"
items={[
{
key: "duration",
label: "目标时长",
children: formatDuration(template.estimated_duration ?? template.target_duration),
},
{
key: "clips",
label: "片段数量",
@@ -87,7 +80,7 @@ export const TemplateDetailModal: React.FC<TemplateDetailModalProps> = ({
]}
/>
<SegmentList segments={segments} totalDuration={totalSegmentDuration} />
<SegmentList segments={segments} />
<StyleConfig template={template} />
<DetailFooter
template={template}
@@ -14,7 +14,6 @@ interface TemplateGridProps {
errorMessage?: string
searchText: string
activeType: string
durationRange: string
onPageChange: (page: number) => void
onPreview: (template: TemplateItem) => void
onToggleFavorite: (id: string, e: React.MouseEvent) => void
@@ -31,7 +30,6 @@ export const TemplateGrid: React.FC<TemplateGridProps> = ({
errorMessage,
searchText,
activeType,
durationRange,
onPageChange,
onPreview,
onToggleFavorite,
@@ -64,7 +62,7 @@ export const TemplateGrid: React.FC<TemplateGridProps> = ({
// 空状态
if (templates.length === 0) {
const hasFilter = !!searchText || activeType !== "全部" || !!durationRange
const hasFilter = !!searchText || activeType !== "全部"
return (
<div className="xx-templates-empty">
<div className="xx-templates-empty-icon">
@@ -1,15 +1,13 @@
import React from "react"
import { SearchOutlined } from "@ant-design/icons"
import type { EditTemplateType, DurationRange } from "../../types/templateLibrary"
import { TEMPLATE_TYPES, DURATION_OPTIONS } from "../../constants/templateLibrary"
import type { EditTemplateType } from "../../types/templateLibrary"
import { TEMPLATE_TYPES } from "../../constants/templateLibrary"
interface TemplateToolbarProps {
searchText: string
onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void
activeType: EditTemplateType | "全部"
onTypeChange: (type: EditTemplateType | "全部") => void
durationRange: DurationRange
onDurationChange: (value: DurationRange) => void
}
export const TemplateToolbar: React.FC<TemplateToolbarProps> = ({
@@ -17,8 +15,6 @@ export const TemplateToolbar: React.FC<TemplateToolbarProps> = ({
onSearchChange,
activeType,
onTypeChange,
durationRange,
onDurationChange,
}) => {
return (
<div className="xx-templates-toolbar">
@@ -46,18 +42,8 @@ export const TemplateToolbar: React.FC<TemplateToolbarProps> = ({
</button>
))}
</div>
{/* 时长筛选 */}
<div className="xx-templates-duration-filter">
{DURATION_OPTIONS.map((opt) => (
<button
key={opt.value}
className={`xx-templates-duration-btn${durationRange === opt.value ? " active" : ""}`}
onClick={() => onDurationChange(opt.value)}
>
{opt.label}
</button>
))}
</div>
</div>
)
}
export default TemplateToolbar
@@ -1,15 +1,13 @@
import React from "react"
import { Tooltip } from "antd"
import type { TemplateSegment } from "@/api/templates"
import { getMaterialTypeLabel, getTypeColor, formatDuration } from "../../../utils/templateLibrary"
import { getMaterialTypeLabel, getTypeColor } from "../../../utils/templateLibrary"
interface SegmentListProps {
segments: TemplateSegment[]
totalDuration: number
}
/** 素材规则 / 片段列表 */
const SegmentList: React.FC<SegmentListProps> = ({ segments, totalDuration }) => {
/** 素材规则 / 片段列表(#1750:模板只定片段数/结构,无时长概念) */
const SegmentList: React.FC<SegmentListProps> = ({ segments }) => {
if (segments.length === 0) return null
return (
@@ -35,17 +33,9 @@ const SegmentList: React.FC<SegmentListProps> = ({ segments, totalDuration }) =>
<span className="xx-template-modal-clip-desc">
{seg.description || `片段 ${seg.segment_order}`}
</span>
<Tooltip title={`时长范围: ${seg.duration_min}秒 - ${seg.duration_max}秒`}>
<span className="xx-template-modal-clip-duration">
{seg.duration_min}-{seg.duration_max}秒
</span>
</Tooltip>
</div>
))}
</div>
<div className="xx-template-modal-total-duration">
预估总时长:{formatDuration(Math.round(totalDuration))}
</div>
</div>
)
}
@@ -1,4 +1,4 @@
import type { EditTemplateType, DurationRange } from "../types/templateLibrary"
import type { EditTemplateType } from "../types/templateLibrary"
export const TEMPLATE_TYPES: Array<{
type: EditTemplateType | "全部"
@@ -15,16 +15,6 @@ export const TEMPLATE_TYPES: Array<{
{ type: "Vlog", label: "Vlog", icon: "📹", color: "#ec4899" },
]
export const DURATION_OPTIONS: Array<{
value: DurationRange
label: string
}> = [
{ value: "", label: "全部时长" },
{ value: "short", label: "30秒以内" },
{ value: "medium", label: "30秒-2分钟" },
{ value: "long", label: "2分钟以上" },
]
export const MATERIAL_TYPE_LABELS: Record<string, string> = {
video: "视频",
image: "图片",
@@ -9,7 +9,7 @@ import {
type TemplateItem,
type TemplateListParams,
} from "@/api/templates"
import type { EditTemplateType, DurationRange } from "../types/templateLibrary"
import type { EditTemplateType } from "../types/templateLibrary"
import { DEFAULT_PAGE_SIZE } from "../constants/templateLibrary"
export const useTemplateLibrary = () => {
@@ -19,7 +19,6 @@ export const useTemplateLibrary = () => {
/* 筛选状态 */
const [searchText, setSearchText] = useState("")
const [activeType, setActiveType] = useState<EditTemplateType | "全部">("全部")
const [durationRange, setDurationRange] = useState<DurationRange>("")
const [page, setPage] = useState(1)
const [pageSize] = useState(DEFAULT_PAGE_SIZE)
@@ -31,9 +30,8 @@ export const useTemplateLibrary = () => {
}
if (activeType !== "全部") params.category = activeType
if (searchText.trim()) params.keyword = searchText.trim()
if (durationRange) params.duration_range = durationRange
return params
}, [page, pageSize, activeType, searchText, durationRange])
}, [page, pageSize, activeType, searchText])
/* 获取模板列表 */
const {
@@ -112,12 +110,6 @@ export const useTemplateLibrary = () => {
setPage(1)
}, [])
/* 切换时长筛选 */
const handleDurationChange = useCallback((value: DurationRange) => {
setDurationRange(value)
setPage(1)
}, [])
return {
/* 状态 */
templates,
@@ -127,7 +119,6 @@ export const useTemplateLibrary = () => {
error,
searchText,
activeType,
durationRange,
page,
pageSize,
/* mutations */
@@ -142,6 +133,5 @@ export const useTemplateLibrary = () => {
handleCreate,
handleSearchChange,
handleCategoryChange,
handleDurationChange,
}
}
@@ -2,7 +2,6 @@
export type EditTemplateType = "口播" | "种草" | "产品" | "品牌" | "混剪" | "Vlog"
/** 时长筛选值 */
export type DurationRange = "" | "short" | "medium" | "long"
/** 配置展示字段 */
export interface ConfigDisplayFields {
@@ -5,7 +5,6 @@ import {
DEFAULT_GRADIENT,
MATERIAL_TYPE_LABELS,
} from "../constants/templateLibrary"
import type { TemplateSegment } from "@/api/templates"
/** 获取类型对应颜色 */
export const getTypeColor = (type: string): string => {
@@ -18,16 +17,6 @@ export const gradientForCategory = (category: string): string => {
return CATEGORY_GRADIENT_MAP[category] ?? DEFAULT_GRADIENT
}
/** 格式化时长 */
export const formatDuration = (seconds: number | undefined | null): string => {
if (!seconds || seconds <= 0) return "0秒"
const totalSec = Math.round(seconds)
const m = Math.floor(totalSec / 60)
const s = totalSec % 60
if (m === 0) return `${s}秒`
return `${m}分${s > 0 ? `${s}秒` : ""}`
}
/** 格式化配置对象为可读文本 */
export const formatConfig = (config?: object): string => {
if (!config || Object.keys(config).length === 0) return "默认"
@@ -47,8 +36,3 @@ export const getMaterialTypeLabel = (materialType: string | null | undefined): s
if (!materialType) return "不限"
return MATERIAL_TYPE_LABELS[materialType] ?? materialType
}
/** 计算片段总时长(取每个片段 min/max 的平均值) */
export const calcTotalSegmentDuration = (segments: TemplateSegment[]): number => {
return segments.reduce((sum, s) => sum + (s.duration_min + s.duration_max) / 2, 0)
}
@@ -41,7 +41,7 @@ export function useVoiceUpload({ voiceLibrary, createLibMutation }: UseVoiceUplo
}
const libs = await queryClient.fetchQuery({
queryKey: ["asset-libraries"],
queryFn: getAssetLibraries,
queryFn: () => getAssetLibraries(),
})
lib = libs.find((l: AssetLibraryItem) => l.kind === "voice")
if (!lib) throw new Error("无法创建配音库")
@@ -24,7 +24,7 @@ export function useVoiceMaterialData({ keyword, gender, tagIds }: UseVoiceMateri
// ── 获取 voice 类型素材库 ─────────────────────────────────
const { data: libraries = [] } = useQuery({
queryKey: ["asset-libraries"],
queryFn: getAssetLibraries,
queryFn: () => getAssetLibraries(),
staleTime: 60_000,
})
@@ -26,7 +26,7 @@ export function useVoiceUpload({ showToast }: UseVoiceUploadProps) {
/* 获取或创建默认配音库 */
const libs = await queryClient.fetchQuery({
queryKey: ["asset-libraries"],
queryFn: getAssetLibraries,
queryFn: () => getAssetLibraries(),
})
const lib = libs.find((l) => l.kind === "voice")
if (!lib) throw new Error("配音库不存在,请先在配音库页面创建")
@@ -0,0 +1,176 @@
/**
* CanvasPreviewGrid 单测(Issue #1741 / #1750)
*
* 验证:
* - N=3 时每个变体都拿到各自的 voiceAudioUrls[i](独立配音模式,null=TTS 合成中)
* - 每个变体都拿到各自的后端真实片段 variantClips[i](serverClips),playToken 为 0 基变体序号
* - 每个变体都收到 activePlayToken / onPlayTokenChange(播放互斥接线)
* - 某个实例上报播放 → 所有实例的 activePlayToken 变为该实例(其他实例收到 token≠自身,自动暂停)
* - 实例上报暂停(null)→ 播放权释放
* - #1750 变体计划申请失败:渲染 role=alert 错误占位 ×N(严禁假数据/不渲染播放器),
* 「重试」按钮仅变体 0 卡片出现且点击触发 onRetryClips
* - #1750 加载态:渲染加载占位,不渲染播放器
*/
import { describe, it, expect, beforeEach, vi } from "vitest"
import { render, fireEvent, screen } from "@testing-library/react"
import type { AssetItem } from "@/api/assets"
import type { EditPlanClip } from "@/api/template-editor"
const playerCalls = vi.hoisted(() => [] as Array<Record<string, unknown>>)
vi.mock("@/pages/generate/components/FrontendPreviewPlayer", () => ({
default: (props: Record<string, unknown>) => {
playerCalls.push(props)
const token = props.playToken as number
const active = props.activePlayToken as number | null
const change = props.onPlayTokenChange as (t: number | null) => void
return (
<div data-testid={`player-${token}`}>
<span data-testid={`voice-${token}`}>{props.voiceAudioUrl ? "has-voice" : "no-voice"}</span>
<span data-testid={`token-${token}`}>{active == null ? "none" : String(active)}</span>
<button type="button" onClick={() => change(token)}>
play-{token}
</button>
<button type="button" onClick={() => change(null)}>
pause-{token}
</button>
</div>
)
},
}))
import CanvasPreviewGrid from "@/pages/generate/components/CanvasPreviewGrid"
function makeAsset(id: string): AssetItem {
return {
id,
library_id: "lib-1",
name: `${id}.mp4`,
storage_key: `media/${id}.mp4`,
file_url: `https://cdn.example.com/${id}.mp4`,
mime_type: "video/mp4",
metadata: { duration: 10 },
duration: 10,
}
}
function makeClips(variant: number): EditPlanClip[] {
return ["a1", "a2"].map((assetId, order) => ({
id: `clip-v${variant}-${assetId}`,
plan_id: `plan-${variant}`,
clip_type: "main",
order,
asset_id: assetId,
text_content: "",
start_time: 0,
duration: 5,
transition_effect: "none",
transition_duration: 0,
playback_speed: 1,
status: "ready",
config: {},
}))
}
const titleSettings = {
size: 36,
font: "思源黑体",
color: "#fff",
position: "bottom" as const,
bold: false,
italic: false,
stroke: true,
shadow: false,
posX: null,
posY: null,
}
const variantClips: EditPlanClip[][] = [makeClips(0), makeClips(1), makeClips(2)]
function renderGrid(overrides: Record<string, unknown> = {}) {
playerCalls.length = 0
return render(
<CanvasPreviewGrid
count={3}
assets={[makeAsset("a1"), makeAsset("a2"), makeAsset("a3")]}
videoRatio="9:16"
titles={["标题1", "标题2", "标题3"]}
titleSettings={titleSettings}
variantClips={variantClips}
voiceAudioUrls={["https://cdn.example.com/v0.mp3", null, "https://cdn.example.com/v2.mp3"]}
selectedIds={[0, 1, 2]}
onToggleSelect={() => {}}
{...overrides}
/>,
)
}
describe("CanvasPreviewGrid 配音、真实片段与播放互斥 (#1741/#1750)", () => {
beforeEach(() => {
playerCalls.length = 0
})
it("N=3 时每个变体都拿到各自的配音 URL 与后端真实片段,playToken 为 0 基序号", () => {
renderGrid()
expect(playerCalls).toHaveLength(3)
playerCalls.forEach((p, i) => {
expect(p.playToken).toBe(i)
expect(p.serverClips).toBe(variantClips[i])
expect(p.variantTitle).toBe(`标题${i + 1}`)
})
// 独立配音:变体 0/2 有各自 URL;变体 1 为 null(TTS 合成中)→ 传 undefined
expect(playerCalls[0].voiceAudioUrl).toBe("https://cdn.example.com/v0.mp3")
expect(playerCalls[1].voiceAudioUrl).toBeUndefined()
expect(playerCalls[2].voiceAudioUrl).toBe("https://cdn.example.com/v2.mp3")
// 配音状态徽标
expect(screen.getByTestId("voice-0").textContent).toBe("has-voice")
expect(screen.getByTestId("voice-1").textContent).toBe("no-voice")
expect(screen.getByTestId("voice-2").textContent).toBe("has-voice")
})
it("每个变体都接线 activePlayToken / onPlayTokenChange", () => {
renderGrid()
playerCalls.forEach((p) => {
expect(p.activePlayToken).toBeNull()
expect(typeof p.onPlayTokenChange).toBe("function")
})
})
it("点击变体2播放:所有实例 activePlayToken 变为 1(0 基,其他实例自动暂停)", () => {
renderGrid()
fireEvent.click(screen.getByTestId("player-1").querySelector("button")!)
expect(screen.getByTestId("token-0").textContent).toBe("1")
expect(screen.getByTestId("token-1").textContent).toBe("1")
expect(screen.getByTestId("token-2").textContent).toBe("1")
})
it("正在播放实例上报暂停后,播放权释放(token 回 null)", () => {
renderGrid()
fireEvent.click(screen.getByTestId("player-2").querySelector("button")!)
expect(screen.getByTestId("token-0").textContent).toBe("2")
fireEvent.click(screen.getByTestId("player-2").querySelectorAll("button")[1])
expect(screen.getByTestId("token-2").textContent).toBe("none")
})
it("clipsError:每张卡片显示错误占位(role=alert)且不渲染任何播放器,重试按钮仅一个并触发回调", () => {
const onRetry = vi.fn()
renderGrid({ clipsError: true, onRetryClips: onRetry })
expect(screen.getAllByRole("alert")).toHaveLength(3)
expect(screen.getAllByText("预览加载失败,请重试")).toHaveLength(3)
// 严禁假数据:错误态不渲染播放器
expect(playerCalls).toHaveLength(0)
// 重试按钮仅变体 0 卡片出现
const retryButtons = screen.getAllByText("重试")
expect(retryButtons).toHaveLength(1)
fireEvent.click(retryButtons[0])
expect(onRetry).toHaveBeenCalledTimes(1)
})
it("clipsLoading:渲染加载占位,不渲染播放器", () => {
renderGrid({ clipsLoading: true })
expect(screen.getAllByText("独立选片中…")).toHaveLength(3)
expect(screen.queryAllByRole("alert")).toHaveLength(0)
expect(playerCalls).toHaveLength(0)
})
})
@@ -0,0 +1,213 @@
/**
* FrontendPreviewPlayer 音频行为单测(Issue #1741 / #1750)
*
* useSegmentScheduler/useCanvasPlayer 用 mock 控制播放态,专注验证本组件的音频逻辑:
* - 有配音时 video 保持 muted(素材原声不与配音混音)
* - 无配音时 video 不 muted(素材原声兜底,保证任何情况下播放有声)
* - 静音按钮:默认有声;点击后切 muted,aria-label 与图标切换
* - 批量播放互斥(#1750 playToken 为 0 基变体序号):activePlayToken 变为其他实例且本实例在播放时,调用 pause
* - 点击播放/暂停时上报播放权(onPlayTokenChange)
* - #1750:serverClips 是唯一片段来源,缺失时不渲染任何 video(无本地模拟 fallback)
*/
import { describe, it, expect, beforeEach, vi } from "vitest"
import { render, screen, fireEvent } from "@testing-library/react"
import FrontendPreviewPlayer from "@/pages/generate/components/FrontendPreviewPlayer"
import type { AssetItem } from "@/api/assets"
import type { EditPlanClip } from "@/api/template-editor"
const mocks = vi.hoisted(() => ({
isPlaying: false,
pause: vi.fn(),
togglePlayPause: vi.fn(),
}))
vi.mock("@/pages/generate/hooks/useSegmentScheduler", () => ({
// segments 由组件 buildPlaybackSegments 产出(唯一来源 serverClips);
// canPlay 与真实 hook 一致:无片段时不可播放
useSegmentScheduler: vi.fn((segments: unknown[]) => ({
isPlaying: mocks.isPlaying,
currentTime: 0,
totalDuration: 20,
currentSegmentIndex: 0,
canPlay: segments.length > 0,
togglePlayPause: mocks.togglePlayPause,
seekTo: vi.fn(),
pause: mocks.pause,
videoRefs: { current: [] as (HTMLVideoElement | null)[] },
})),
}))
vi.mock("@/pages/generate/hooks/useCanvasPlayer", () => ({
useCanvasPlayer: () => ({
state: {
isPlaying: false,
isReady: false,
isBuffering: false,
currentTime: 0,
duration: 0,
errorMessage: "",
hasDecodeError: false,
},
controls: { play: vi.fn(), pause: vi.fn(), seek: vi.fn() },
}),
}))
beforeEach(() => {
mocks.isPlaying = false
mocks.pause.mockClear()
mocks.togglePlayPause.mockClear()
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
unobserve() {}
disconnect() {}
},
)
})
function makeAsset(id: string): AssetItem {
return {
id,
library_id: "lib-1",
name: `${id}.mp4`,
storage_key: `media/${id}.mp4`,
file_url: `https://cdn.example.com/${id}.mp4`,
mime_type: "video/mp4",
metadata: { duration: 10, width: 1080, height: 1920 },
duration: 10,
}
}
function makeClip(assetId: string, order: number): EditPlanClip {
return {
id: `clip-${assetId}`,
plan_id: "plan-1",
clip_type: "main",
order,
asset_id: assetId,
text_content: "",
start_time: 0,
duration: 5,
transition_effect: "none",
transition_duration: 0,
playback_speed: 1,
status: "ready",
config: {},
}
}
const serverClips = [makeClip("a1", 0), makeClip("a2", 1)]
const baseProps = {
assets: [makeAsset("a1"), makeAsset("a2")],
videoRatio: "9:16",
ready: true,
serverClips,
}
function videos(): HTMLVideoElement[] {
return Array.from(document.querySelectorAll("video"))
}
describe("FrontendPreviewPlayer 音频行为 (#1741/#1750)", () => {
it("有配音时 video 保持 muted(素材原声不与配音混音)", () => {
render(<FrontendPreviewPlayer {...baseProps} voiceAudioUrl="https://cdn.example.com/tts.mp3" />)
expect(videos()).toHaveLength(2)
videos().forEach((v) => expect(v.muted).toBe(true))
})
it("无配音时 video 不 muted(素材原声兜底)", () => {
render(<FrontendPreviewPlayer {...baseProps} />)
videos().forEach((v) => expect(v.muted).toBe(false))
})
it("无配音时点静音按钮,video 切换为 muted;再点恢复", () => {
render(<FrontendPreviewPlayer {...baseProps} />)
const vs = videos()
expect(vs[0].muted).toBe(false)
fireEvent.click(screen.getByRole("button", { name: "静音" }))
videos().forEach((v) => expect(v.muted).toBe(true))
fireEvent.click(screen.getByRole("button", { name: "取消静音" }))
videos().forEach((v) => expect(v.muted).toBe(false))
})
it("批量播放互斥:token 变为其他实例且本实例在播放时调用 pause", () => {
mocks.isPlaying = true
const { rerender } = render(
<FrontendPreviewPlayer
{...baseProps}
playToken={1}
activePlayToken={1}
onPlayTokenChange={() => {}}
compact
/>,
)
expect(mocks.pause).not.toHaveBeenCalled()
// 播放权切给实例 2(0 基 token)
rerender(
<FrontendPreviewPlayer
{...baseProps}
playToken={1}
activePlayToken={2}
onPlayTokenChange={() => {}}
compact
/>,
)
expect(mocks.pause).toHaveBeenCalledTimes(1)
// token 切回自己:不重复暂停
rerender(
<FrontendPreviewPlayer
{...baseProps}
playToken={1}
activePlayToken={1}
onPlayTokenChange={() => {}}
compact
/>,
)
expect(mocks.pause).toHaveBeenCalledTimes(1)
})
it("未播放时 token 变化不触发暂停(effect 仅在本实例播放时生效)", () => {
// mocks.isPlaying = false(beforeEach 重置)
const { rerender } = render(
<FrontendPreviewPlayer {...baseProps} playToken={0} activePlayToken={0} compact />,
)
rerender(<FrontendPreviewPlayer {...baseProps} playToken={0} activePlayToken={1} compact />)
expect(mocks.pause).not.toHaveBeenCalled()
})
it("暂停状态下点击播放按钮:上报播放权为自身 playToken 并触发播放", () => {
const onToken = vi.fn()
render(
<FrontendPreviewPlayer {...baseProps} playToken={2} onPlayTokenChange={onToken} compact />,
)
// 暂停态有两个图标播放按钮(中央大按钮 + 控制条按钮),均调 handleTogglePlay,点中央那个
const playButtons = screen.getAllByRole("button").filter((b) => !b.getAttribute("aria-label"))
expect(playButtons.length).toBeGreaterThanOrEqual(1)
fireEvent.click(playButtons[0])
expect(onToken).toHaveBeenCalledWith(2)
expect(mocks.togglePlayPause).toHaveBeenCalledTimes(1)
})
it("每个卡片都渲染独立静音按钮", () => {
render(
<div>
<FrontendPreviewPlayer {...baseProps} playToken={0} compact />
<FrontendPreviewPlayer {...baseProps} playToken={1} compact />
<FrontendPreviewPlayer {...baseProps} playToken={2} compact />
</div>,
)
expect(screen.getAllByRole("button", { name: "静音" })).toHaveLength(3)
})
it("#1750 serverClips 缺失时不渲染任何 video(无本地模拟 fallback,显示无可播放素材)", () => {
render(<FrontendPreviewPlayer {...baseProps} serverClips={undefined} />)
expect(videos()).toHaveLength(0)
expect(screen.getByText("暂无可播放素材")).toBeInTheDocument()
})
})
@@ -0,0 +1,40 @@
/**
* 智能匹配候选池数量规则单测(#1744/#1750)
*
* #1750 基线:模板只有片段数/结构、没有时长概念——候选池纯按「片段数 × 3」
* (每个片段至少 3 个候选供变体间洗牌避让),不再参考任何 duration 字段;
* 结果钳制到 [1, 200](后端 limit 上限 200);无片段信息时兜底 30。
*/
import { describe, it, expect } from "vitest"
import { computeLimitFromSegments } from "@/pages/generate/hooks/step2-materials/useSmartMatch"
import type { TemplateSegment } from "@/api/templates/types"
function segs(n: number): TemplateSegment[] {
// 新规则只看片段数量,duration 字段已废弃,这里不再构造
return Array.from({ length: n }, () => ({}) as TemplateSegment)
}
describe("computeLimitFromSegments (#1750 候选池 = 片段数×3)", () => {
it("无 segments 时返回兜底 30", () => {
expect(computeLimitFromSegments(undefined)).toBe(30)
expect(computeLimitFromSegments([])).toBe(30)
})
it("候选池 = 片段数 × 3(与任何时长字段无关)", () => {
expect(computeLimitFromSegments(segs(1))).toBe(3)
expect(computeLimitFromSegments(segs(3))).toBe(9)
expect(computeLimitFromSegments(segs(5))).toBe(15)
expect(computeLimitFromSegments(segs(10))).toBe(30)
})
it("超过上限钳制为 200(后端 limit 上限)", () => {
expect(computeLimitFromSegments(segs(67))).toBe(200) // 67×3=201 → 200
expect(computeLimitFromSegments(segs(100))).toBe(200) // 100×3=300 → 200
})
it("任何正常片段数下 limit 均 ≥ 1", () => {
for (let n = 0; n <= 10; n++) {
expect(computeLimitFromSegments(segs(n))).toBeGreaterThanOrEqual(1)
}
})
})
@@ -5,7 +5,6 @@
*
* 重构记录:
* - useStep2Materials 拆分为 useMaterialLibrary + useSmartMatch 子 Hook
* - Phase1 预览生成:新增 generation API 层 + PreviewVideoPanel
*/
import { describe, it, expect } from "vitest"
@@ -23,8 +22,6 @@ import "@/pages/generate/components/Step3VoiceWithMode"
import "@/pages/generate/components/CanvasPreviewGrid"
import "@/pages/generate/components/BatchGenerationGrid"
import "@/pages/generate/components/PreviewCountModal"
import "@/pages/generate/components/PreviewVideoPanel"
import "@/pages/generate/components/GenerateResultPanel"
import "@/pages/generate/components/GenerateStepContent"
import "@/pages/generate/components/voice/VoiceRecommendSection"
import "@/pages/generate/components/voice/VoiceChoiceCard"
@@ -50,6 +47,8 @@ describe("GeneratePage module smoke test", () => {
})
import "@/pages/generate/hooks/useGenerateVideo"
import "@/pages/generate/hooks/useBatchCovers"
import "@/pages/generate/hooks/useBatchVariantPlans"
import "@/pages/generate/hooks/useVariantVoicePreview"
import "@/pages/generate/hooks/usePreviewAssets"
import "@/pages/generate/hooks/useSegmentScheduler"
import "@/pages/generate/hooks/generate-video/useGenerationPolling"
@@ -0,0 +1,140 @@
/**
* 失效模板判定/回退纯函数单测(#1777)
*/
import { describe, it, expect } from "vitest"
import type { EditingTemplate } from "@/api/editing-planner"
import {
getHttpStatus,
isInvalidTemplateError,
isValidTemplate,
findFirstValidTemplate,
} from "@/pages/generate/hooks/useGenerateFormState/templateFallback"
function makeTemplate(partial: Partial<EditingTemplate> & { id: string }): EditingTemplate {
return {
name: partial.id,
mode: "pip",
category: "默认",
tags: [],
title_config: {
ai_auto_select: false,
content: "",
font_preset: "",
font_color: "",
font_size: 28,
position: "top",
},
subtitle_config: {
enabled: true,
position: "bottom",
font: "",
color: "",
size: 20,
animation: "",
},
bgm_config: { enabled: false, music_id: "" },
segments: [{ segment_order: 0, material_type: null }],
is_active: true,
created_at: "",
updated_at: "",
...partial,
} as EditingTemplate
}
function axiosError(status: number, data?: unknown) {
return { isAxiosError: true, response: { status, data } }
}
describe("getHttpStatus", () => {
it("提取 axios 错误的 HTTP 状态码", () => {
expect(getHttpStatus(axiosError(404))).toBe(404)
expect(getHttpStatus(axiosError(400))).toBe(400)
})
it("非 axios/无响应错误返回 null", () => {
expect(getHttpStatus(new Error("network"))).toBeNull()
expect(getHttpStatus(null)).toBeNull()
expect(getHttpStatus(undefined)).toBeNull()
expect(getHttpStatus({ isAxiosError: true })).toBeNull()
})
})
describe("isInvalidTemplateError", () => {
it("404 始终判定为模板失效(模板不存在)", () => {
expect(isInvalidTemplateError(axiosError(404))).toBe(true)
expect(isInvalidTemplateError(axiosError(404, { detail: "Not Found" }))).toBe(true)
})
it("400 且后端文案提到「片段配置」判定为模板无片段配置", () => {
expect(
isInvalidTemplateError(axiosError(400, { detail: "模板没有片段配置,无法创建片段" })),
).toBe(true)
})
it("400 但文案与片段配置无关 → 不误判", () => {
expect(isInvalidTemplateError(axiosError(400, { detail: "素材参数错误" }))).toBe(false)
})
it("422 命中片段/模板字段判定为失效", () => {
expect(
isInvalidTemplateError(
axiosError(422, { detail: [{ loc: ["body", "segments"], msg: "field required" }] }),
),
).toBe(true)
})
it("其他状态码(401/403/500/超时/网络)不判定为模板失效", () => {
expect(isInvalidTemplateError(axiosError(401))).toBe(false)
expect(isInvalidTemplateError(axiosError(403))).toBe(false)
expect(isInvalidTemplateError(axiosError(500))).toBe(false)
expect(isInvalidTemplateError({ code: "ECONNABORTED", message: "timeout of 60000ms" })).toBe(
false,
)
expect(isInvalidTemplateError(new Error("Network Error"))).toBe(false)
})
})
describe("isValidTemplate", () => {
it("有片段且未被标记 inactive → 有效", () => {
expect(isValidTemplate(makeTemplate({ id: "t1" }))).toBe(true)
})
it("segments 为空 → 无效(无片段配置)", () => {
expect(isValidTemplate(makeTemplate({ id: "t2", segments: [] }))).toBe(false)
})
it("is_active=false → 无效(已停用/删除)", () => {
expect(isValidTemplate(makeTemplate({ id: "t3", is_active: false }))).toBe(false)
})
it("is_active 字段缺失时视为有效(兼容旧后端)", () => {
const t = makeTemplate({ id: "t4" })
delete (t as Partial<EditingTemplate>).is_active
expect(isValidTemplate(t)).toBe(true)
})
it("null/undefined → 无效", () => {
expect(isValidTemplate(null)).toBe(false)
expect(isValidTemplate(undefined)).toBe(false)
})
})
describe("findFirstValidTemplate", () => {
it("跳过无效模板,返回第一个有效模板", () => {
const list = [
makeTemplate({ id: "empty", segments: [] }),
makeTemplate({ id: "inactive", is_active: false }),
makeTemplate({ id: "valid1" }),
makeTemplate({ id: "valid2" }),
]
expect(findFirstValidTemplate(list)?.id).toBe("valid1")
})
it("全部无效 → null(用于空状态引导)", () => {
expect(
findFirstValidTemplate([
makeTemplate({ id: "a", segments: [] }),
makeTemplate({ id: "b", is_active: false }),
]),
).toBeNull()
})
it("空数组/null → null", () => {
expect(findFirstValidTemplate([])).toBeNull()
expect(findFirstValidTemplate(null)).toBeNull()
expect(findFirstValidTemplate(undefined)).toBeNull()
})
})
@@ -0,0 +1,230 @@
/**
* useBatchVariantPlans 单测(#1750 修正:严禁假数据冒充预览)
* - 批量(N>1)时向后端申请变体计划,返回 clips/planIds
* - 404(端点未上线)/ 400(参数错误)/ 网络错误:一律 error=true,UI 可重试
* - 数据不完整(某变体 clips 为空):error=true
* - retry() 重新发起请求,成功后 ready=true
* - N=1 / 无素材 / disabled 不发请求
* - 入参变化重新申请;未变化不重复请求
*/
import { describe, it, expect, vi, beforeEach } from "vitest"
import { renderHook, waitFor, act } from "@testing-library/react"
import { useBatchVariantPlans } from "@/pages/generate/hooks/useBatchVariantPlans"
import type { EditPlanClip } from "@/api/template-editor"
vi.mock("@/api/generation/variantPlans", () => ({
createBatchVariantPlans: vi.fn(),
}))
import { createBatchVariantPlans } from "@/api/generation/variantPlans"
const mockCreate = vi.mocked(createBatchVariantPlans)
function makeClip(
partial: Partial<EditPlanClip> & { asset_id: string; order: number },
): EditPlanClip {
return {
id: `clip-${partial.asset_id}-${partial.order}`,
plan_id: "plan-x",
clip_type: "main",
start_time: 0,
duration: 5,
text_content: "",
transition_effect: "",
transition_duration: 0,
playback_speed: 1,
status: "ready",
config: {},
...partial,
}
}
function buildResp(count: number) {
return {
total: count,
items: Array.from({ length: count }, (_, i) => ({
variant_index: i,
plan_id: `plan-${i}`,
clips: [makeClip({ asset_id: `a${(i % 3) + 1}`, order: 0, start_time: i * 2 })],
})),
}
}
beforeEach(() => {
vi.clearAllMocks()
mockCreate.mockReset()
})
describe("useBatchVariantPlans (#1750 错误态)", () => {
it("批量 N=3 时申请变体计划并返回按索引对齐的 clips/planIds", async () => {
mockCreate.mockResolvedValueOnce(buildResp(3))
const { result } = renderHook(() =>
useBatchVariantPlans({
enabled: true,
count: 3,
templateId: "tpl-1",
assetIds: ["a1", "a2", "a3"],
sourcePlanId: "plan-src",
}),
)
await waitFor(() => expect(result.current.ready).toBe(true))
expect(mockCreate).toHaveBeenCalledTimes(1)
expect(mockCreate).toHaveBeenCalledWith({
template_id: "tpl-1",
asset_ids: ["a1", "a2", "a3"],
count: 3,
source_edit_plan_id: "plan-src",
})
expect(result.current.planIdsByVariant).toEqual(["plan-0", "plan-1", "plan-2"])
expect(result.current.clipsByVariant[1]?.[0]?.start_time).toBe(2)
expect(result.current.error).toBe(false)
expect(result.current.loading).toBe(false)
})
it("sourcePlanId 为空时不传 source_edit_plan_id 字段", async () => {
mockCreate.mockResolvedValueOnce(buildResp(2))
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 2, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.ready).toBe(true))
const arg = mockCreate.mock.calls[0][0]
expect(arg).not.toHaveProperty("source_edit_plan_id")
})
it("404(后端端点未上线)→ error=true,不再静默降级", async () => {
mockCreate.mockRejectedValueOnce({ response: { status: 404 } })
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 3, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.loading).toBe(false))
expect(result.current.error).toBe(true)
expect(result.current.ready).toBe(false)
expect(result.current.clipsByVariant).toEqual([])
})
it("400(参数错误)→ error=true", async () => {
mockCreate.mockRejectedValueOnce({ response: { status: 400, data: { detail: "参数错误" } } })
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 3, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.loading).toBe(false))
expect(result.current.error).toBe(true)
expect(result.current.ready).toBe(false)
})
it("网络错误 → error=true", async () => {
mockCreate.mockRejectedValue(new Error("Network Error"))
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 3, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.loading).toBe(false))
expect(result.current.error).toBe(true)
expect(result.current.ready).toBe(false)
})
it("某变体 clips 为空(数据不完整)→ error=true,严禁假数据冒充", async () => {
mockCreate.mockResolvedValueOnce({
total: 2,
items: [
{ variant_index: 0, plan_id: "plan-0", clips: [makeClip({ asset_id: "a1", order: 0 })] },
{ variant_index: 1, plan_id: "plan-1", clips: [] },
],
})
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 2, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.loading).toBe(false))
expect(result.current.error).toBe(true)
expect(result.current.ready).toBe(false)
expect(result.current.clipsByVariant).toEqual([])
})
it("retry() 失败后重试:成功则 error 清除、ready=true", async () => {
mockCreate.mockRejectedValueOnce({ response: { status: 404 } })
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 3, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.error).toBe(true))
mockCreate.mockResolvedValueOnce(buildResp(3))
act(() => {
result.current.retry()
})
await waitFor(() => expect(result.current.ready).toBe(true))
expect(result.current.error).toBe(false)
expect(mockCreate).toHaveBeenCalledTimes(2)
})
it("N=1 不发请求(单视频零回归)", () => {
renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 1, templateId: "tpl-1", assetIds: ["a1"] }),
)
expect(mockCreate).not.toHaveBeenCalled()
})
it("无素材/enabled=false 不发请求", () => {
const { rerender } = renderHook(
(props: { enabled: boolean; ids: string[] }) =>
useBatchVariantPlans({
enabled: props.enabled,
count: 3,
templateId: "tpl-1",
assetIds: props.ids,
}),
{ initialProps: { enabled: true, ids: [] as string[] } },
)
expect(mockCreate).not.toHaveBeenCalled()
rerender({ enabled: false, ids: ["a1"] })
expect(mockCreate).not.toHaveBeenCalled()
})
it("响应乱序/越界 variant_index:越界丢弃、其余按索引对齐", async () => {
mockCreate.mockResolvedValueOnce({
total: 2,
items: [
{ variant_index: 99, plan_id: "plan-bad", clips: [makeClip({ asset_id: "a9", order: 0 })] },
{ variant_index: 1, plan_id: "plan-1", clips: [makeClip({ asset_id: "a2", order: 0 })] },
{ variant_index: 0, plan_id: "plan-0", clips: [makeClip({ asset_id: "a1", order: 0 })] },
],
})
const { result } = renderHook(() =>
useBatchVariantPlans({
enabled: true,
count: 2,
templateId: "tpl-1",
assetIds: ["a1", "a2"],
}),
)
await waitFor(() => expect(result.current.ready).toBe(true))
// 越界 99 丢弃;乱序返回的 0/1 按索引对齐
expect(result.current.error).toBe(false)
expect(result.current.planIdsByVariant).toEqual(["plan-0", "plan-1"])
expect(result.current.clipsByVariant[0]?.[0]?.asset_id).toBe("a1")
expect(result.current.clipsByVariant[1]?.[0]?.asset_id).toBe("a2")
})
it("非 ready 状态的 clip 被过滤;过滤后每变体仍有片段则 ready", async () => {
mockCreate.mockResolvedValueOnce({
total: 2,
items: [
{
variant_index: 0,
plan_id: "plan-0",
clips: [
makeClip({ asset_id: "a1", order: 0 }),
makeClip({ asset_id: "a2", order: 1, status: "pending" }),
],
},
{ variant_index: 1, plan_id: "plan-1", clips: [makeClip({ asset_id: "a3", order: 0 })] },
],
})
const { result } = renderHook(() =>
useBatchVariantPlans({ enabled: true, count: 2, templateId: "tpl-1", assetIds: ["a1"] }),
)
await waitFor(() => expect(result.current.ready).toBe(true))
expect(result.current.error).toBe(false)
expect(result.current.clipsByVariant[0]).toHaveLength(1)
expect(result.current.clipsByVariant[0]?.[0]?.asset_id).toBe("a1")
})
})
@@ -0,0 +1,74 @@
/**
* useMaterialLibrary Hook 单测(#1777)
* - Step2 视频库选择器只拉取 kind=video 的素材库,配音库(voice)/图片库(image) 不混入
* - 自动选中第一个视频库
*/
import { describe, it, expect, vi, beforeEach } from "vitest"
import { renderHook, waitFor } from "@testing-library/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import type { ReactNode } from "react"
import type { AssetItem, AssetLibraryItem } from "@/api/assets"
vi.mock("@/api/assets", () => ({
getAssetLibraries: vi.fn(),
getAssets: vi.fn(),
isAssetUsable: vi.fn(() => true),
}))
import { getAssetLibraries, getAssets } from "@/api/assets"
import { useMaterialLibrary } from "@/pages/generate/hooks/step2-materials/useMaterialLibrary"
const mockGetLibraries = vi.mocked(getAssetLibraries)
const mockGetAssets = vi.mocked(getAssets)
function lib(id: string, kind: AssetLibraryItem["kind"], name = id): AssetLibraryItem {
return { id, name, kind }
}
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
})
return ({ children }: { children: ReactNode }) =>
(<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>) as ReactNode
}
beforeEach(() => {
vi.clearAllMocks()
mockGetAssets.mockResolvedValue({ items: [] as AssetItem[], total: 0 })
})
describe("useMaterialLibrary (#1777 kind=video 过滤)", () => {
it("按 kind=video 拉取素材库(后端参数过滤)", async () => {
mockGetLibraries.mockResolvedValueOnce([lib("v1", "video")])
renderHook(() => useMaterialLibrary(), { wrapper: createWrapper() })
await waitFor(() => expect(mockGetLibraries).toHaveBeenCalledTimes(1))
expect(mockGetLibraries).toHaveBeenCalledWith("video")
})
it("下拉库列表只包含视频库(自动选中第一个视频库)", async () => {
mockGetLibraries.mockResolvedValueOnce([
lib("voice-1", "voice"),
lib("img-1", "image"),
lib("video-1", "video"),
lib("video-2", "video"),
])
const { result } = renderHook(() => useMaterialLibrary(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.libraries).toHaveLength(2))
expect(result.current.libraries.map((l) => l.id)).toEqual(["video-1", "video-2"])
expect(result.current.libraries.every((l) => l.kind === "video")).toBe(true)
// 自动选中第一个视频库
expect(result.current.selectedLibraryId).toBe("video-1")
})
it("没有视频库时库列表为空且不自动选中(UI 展示空状态)", async () => {
mockGetLibraries.mockResolvedValueOnce([lib("voice-1", "voice"), lib("img-1", "image")])
const { result } = renderHook(() => useMaterialLibrary(), { wrapper: createWrapper() })
await waitFor(() => expect(mockGetLibraries).toHaveBeenCalled())
expect(result.current.libraries).toEqual([])
expect(result.current.selectedLibraryId).toBe("")
})
})
@@ -0,0 +1,174 @@
/**
* useTemplateSelection Hook 单测(#1777)
* - 自动选择跳过无片段/inactive 模板,只选第一个有效模板
* - 传 validOnly=true 给后端
* - 用户手动选择优先,自动逻辑不覆盖
* - handleInvalidTemplate:失效时自动切到第一个有效模板 + toast;无有效模板时清空
* - selectedTemplate 仅内存态,不写入 localStorage/sessionStorage
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import { renderHook, waitFor, act } from "@testing-library/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import type { ReactNode } from "react"
// antd message mock(拦截 toast)——vi.hoisted 保证 mock 工厂可引用
const { messageMock } = vi.hoisted(() => ({
messageMock: {
warning: vi.fn(),
error: vi.fn(),
success: vi.fn(),
info: vi.fn(),
loading: vi.fn(() => vi.fn()),
},
}))
vi.mock("antd", () => ({ message: messageMock }))
vi.mock("@/api/editing-planner", () => ({
getEditingTemplates: vi.fn(),
}))
import { getEditingTemplates } from "@/api/editing-planner"
import type { EditingTemplate } from "@/api/editing-planner"
import { useTemplateSelection } from "@/pages/generate/hooks/useGenerateFormState/useTemplateSelection"
const mockGetTemplates = vi.mocked(getEditingTemplates)
function tpl(id: string, partial: Partial<EditingTemplate> = {}): EditingTemplate {
return {
id,
name: id,
mode: "pip",
category: "默认",
tags: [],
title_config: {
ai_auto_select: false,
content: "",
font_preset: "",
font_color: "",
font_size: 28,
position: "top",
},
subtitle_config: {
enabled: true,
position: "bottom",
font: "",
color: "",
size: 20,
animation: "",
},
bgm_config: { enabled: false, music_id: "" },
segments: [{ segment_order: 0, material_type: null }],
is_active: true,
created_at: "",
updated_at: "",
...partial,
} as EditingTemplate
}
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, gcTime: 0 } },
})
return ({ children }: { children: ReactNode }) =>
(<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>) as ReactNode
}
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
sessionStorage.clear()
})
afterEach(() => {
localStorage.clear()
sessionStorage.clear()
})
describe("useTemplateSelection (#1777)", () => {
it("请求模板时传 validOnly=true,并自动选中第一个有片段的有效模板", async () => {
mockGetTemplates.mockResolvedValueOnce([
tpl("empty", { segments: [] }),
tpl("inactive", { is_active: false }),
tpl("valid-a"),
tpl("valid-b"),
])
const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.selectedTemplate).toBe("valid-a"))
expect(mockGetTemplates).toHaveBeenCalledWith({ validOnly: true })
// 暴露给 UI 的 userTemplates 已过滤掉无效模板
expect(result.current.userTemplates.map((t) => t.id)).toEqual(["valid-a", "valid-b"])
})
it("列表全部无效时 selectedTemplate 为空(交空状态引导),不选中失效模板", async () => {
mockGetTemplates.mockResolvedValueOnce([
tpl("empty", { segments: [] }),
tpl("inactive", { is_active: false }),
])
const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() })
await waitFor(() => expect(mockGetTemplates).toHaveBeenCalled())
// 给 effect 一个 tick
await waitFor(() => expect(result.current.selectedTemplate).toBe(""))
expect(result.current.userTemplates).toHaveLength(0)
})
it("用户手动选择优先:自动逻辑不会覆盖手动选择", async () => {
mockGetTemplates.mockResolvedValueOnce([tpl("a"), tpl("b")])
const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.selectedTemplate).toBe("a"))
act(() => result.current.setSelectedTemplate("b"))
expect(result.current.selectedTemplate).toBe("b")
// 重新渲染 / refetch 后仍保持用户的手动选择
await waitFor(() => expect(result.current.selectedTemplate).toBe("b"))
})
it("handleInvalidTemplate:当前模板失效时自动切到第一个有效模板并 toast", async () => {
mockGetTemplates.mockResolvedValueOnce([tpl("bad", { segments: [] }), tpl("good")])
const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() })
// 自动选中有效模板 good(bad 无片段不会被自动选中)
await waitFor(() => expect(result.current.selectedTemplate).toBe("good"))
messageMock.warning.mockClear()
// 模拟运行时用户停留在一个已失效的模板 id(外部/草稿态),触发回退
act(() => result.current.setSelectedTemplate("stale-id"))
expect(result.current.selectedTemplate).toBe("stale-id")
act(() => {
const handled = result.current.handleInvalidTemplate()
expect(handled).toBe(true)
})
await waitFor(() => expect(result.current.selectedTemplate).toBe("good"))
expect(messageMock.warning).toHaveBeenCalledWith("原模板已失效,已自动切换")
})
it("handleInvalidTemplate:无有效模板时清空选择并提示去创建", async () => {
mockGetTemplates.mockResolvedValueOnce([tpl("bad", { segments: [] })])
const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.userTemplates).toHaveLength(0))
act(() => result.current.setSelectedTemplate("stale-id"))
act(() => {
result.current.handleInvalidTemplate()
})
await waitFor(() => expect(result.current.selectedTemplate).toBe(""))
expect(messageMock.warning).toHaveBeenCalledWith(expect.stringContaining("没有可用模板"))
})
it("失效模板 ID 不写入任何持久化存储", async () => {
mockGetTemplates.mockResolvedValueOnce([tpl("good")])
const { result } = renderHook(() => useTemplateSelection(), { wrapper: createWrapper() })
await waitFor(() => expect(result.current.selectedTemplate).toBe("good"))
act(() => result.current.setSelectedTemplate("stale-invalid-id"))
act(() => result.current.handleInvalidTemplate())
const ls = JSON.stringify(localStorage)
const ss = JSON.stringify(sessionStorage)
expect(ls).not.toContain("stale-invalid-id")
expect(ss).not.toContain("stale-invalid-id")
// URL 也不含
expect(window.location.href).not.toContain("stale-invalid-id")
})
})
+31 -4
View File
@@ -39,6 +39,8 @@ class BGMConfig:
sidechain_attack: float = 0.02 # 攻击时间
sidechain_release: float = 0.5 # 释放时间
sidechain_threshold: float = -25.0 # 触发阈值(dB)
audio_offset: float = 0.0 # BGM 段落起始偏移(秒),#1767 策略二
volume_adjust_db: float = 0.0 # 音量微调 dB(-3~+3),#1767 策略三
@classmethod
def from_config_dict(cls, bgm_path: str, config: dict) -> "BGMConfig":
@@ -54,6 +56,8 @@ class BGMConfig:
sidechain_attack=float(config.get("sidechain_attack", 0.02)),
sidechain_release=float(config.get("sidechain_release", 0.5)),
sidechain_threshold=float(config.get("sidechain_threshold", -25.0)),
audio_offset=float(config.get("audio_offset", 0.0)),
volume_adjust_db=float(config.get("volume_adjust_db", 0.0)),
)
@@ -84,14 +88,18 @@ def prepare_bgm_track(
target_duration = 5.0 # 兜底
bgm_dur = probe_duration(bgm.bgm_path)
needs_loop = bgm.loop_enabled and bgm_dur > 0 and bgm_dur < target_duration * 0.9
# #1767:seek 后有效时长 = 总时长 - 偏移
effective_dur = (
max(1.0, bgm_dur - bgm.audio_offset) if bgm.audio_offset > 0 and bgm_dur > bgm.audio_offset else bgm_dur
)
needs_loop = bgm.loop_enabled and effective_dur > 0 and effective_dur < target_duration * 0.9
# 构建滤镜链
filter_parts: list[str] = []
if needs_loop:
# 计算需要循环多少次才能铺满
loop_count = max(1, int(target_duration / bgm_dur) + 2)
# 计算需要循环多少次才能铺满(基于 seek 后有效时长)
loop_count = max(1, int(target_duration / effective_dur) + 2)
# aloop 滤镜:循环指定次数
filter_parts.append(f"aloop=loop={loop_count}:size=0")
@@ -113,11 +121,30 @@ def prepare_bgm_track(
filter_parts.append(f"atrim=0:{target_duration:.3f}")
filter_parts.append("asetpts=N/SR/TB") # 重置时间戳
filter_str = ",".join(filter_parts)
# #1767:BGM 段落差异化 — 使用 -ss 从偏移位置开始(seek 效率高,不读跳过部分)
seek_args: list[str] = []
if bgm.audio_offset > 0 and bgm_dur > bgm.audio_offset:
seek_args = ["-ss", f"{bgm.audio_offset:.3f}"]
logger.info("[bgm] #1767 audio_offset=%.1fs(段落差异化)", bgm.audio_offset)
# #1767:音量微调 — dB 转线性系数(10^(dB/20))
db_adjust_filter = ""
if abs(bgm.volume_adjust_db) > 0.01:
linear_factor = 10.0 ** (bgm.volume_adjust_db / 20.0)
db_adjust_filter = f",volume={linear_factor:.4f}"
logger.info("[bgm] #1767 volume_adjust=%.0fdB → linear=%.4f", bgm.volume_adjust_db, linear_factor)
# #1767:追加 dB 微调到滤镜链末尾
if db_adjust_filter:
filter_str_base = ",".join(filter_parts)
filter_str = filter_str_base + db_adjust_filter
else:
filter_str = ",".join(filter_parts)
command = [
FFMPEG_BIN,
"-y",
*seek_args,
"-i",
bgm.bgm_path,
"-filter:a",
+181 -10
View File
@@ -7,6 +7,7 @@ import hashlib
import logging
import math
import os
import re
import statistics
import tempfile
from dataclasses import dataclass, field
@@ -56,8 +57,13 @@ MAX_GAP = 2 # 允许的最大间隙帧数
NEIGHBOR_WINDOW = 1 # 分片时序对齐:允许 ±1 邻接偏移(1s 密集采样下即 ±1s,缓解切点不一致)
# ── 融合判定常量 ────────────────────────────────────────────────
PHASH_WEIGHT = 0.7 # pHash 权重
HISTOGRAM_WEIGHT = 0.3 # 直方图权重
PHASH_WEIGHT = 0.7 # pHash 权重(视觉内部)
HISTOGRAM_WEIGHT = 0.3 # 直方图权重(视觉内部)
# ── 多维度查重融合权重(Issue #P2-后端3) ────────────────────────
VISUAL_WEIGHT = 0.5 # 视觉相似度权重(pHash+直方图)
TEXT_WEIGHT = 0.25 # 文案相似度权重(配音文本)
STRUCTURE_WEIGHT = 0.25 # 结构相似度权重(片段序列)
MATCH_RATIO_THRESHOLD = 0.7 # 全片重复(is_duplicate)至少 70% 帧匹配
PARTIAL_COVERAGE_THRESHOLD = 0.5 # 局部复用覆盖率 >=50% 也判全片重复
DUPLICATE_THRESHOLD = 0.70 # 融合后相似度阈值
@@ -1057,12 +1063,11 @@ class VideoDeduplicator:
) -> dict:
"""计算当前视频与已有视频的查重率百分比。
新公式(双指标加权):
- frame_match_rate = 汉明距离 < PHASH_THRESHOLD 的帧数 / 总帧数
- temporal_coverage_rate = 连续匹配片段总时长 / 视频总时长
- duplicate_rate = (frame_match_rate * 0.4 + temporal_coverage_rate * 0.6) * 100
visual_similarity = 0.7 * phash_sim + 0.3 * hist_sim(归一化到 0~1)
多维度融合公式(Issue #P2-后端3):
- visual_similarity = 0.7 * phash_sim + 0.3 * hist_sim(视觉维度)
- text_similarity = 文案 Jaccard 相似度(文案维度)
- structure_similarity = 片段序列相似度(结构维度)
- duplicate_rate = (visual*0.5 + text*0.25 + structure*0.25) * 100
对每个匹配视频都算,取最高 duplicate_rate。
@@ -1093,6 +1098,29 @@ class VideoDeduplicator:
match_count = 0
evaluated = 0
# Issue #P2-后端3: 加载当前视频的文案+结构数据
from packages.adapters.sqlalchemy_impl.models import EditPlanClipModel, GeneratedVideoModel
current_video_obj = (
session.query(GeneratedVideoModel).filter(GeneratedVideoModel.id == current_video_id).first()
if current_video_id
else None
)
current_plan_id = getattr(current_video_obj, "edit_plan_id", "") or ""
current_clips_data = []
current_text_content = ""
if current_plan_id:
current_clips = (
session.query(EditPlanClipModel)
.filter(EditPlanClipModel.plan_id == current_plan_id)
.order_by(EditPlanClipModel.order)
.all()
)
current_clips_data = [{"clip_type": c.clip_type, "duration": c.duration} for c in current_clips]
# 拼接所有片段的文本内容
current_text_content = " ".join(c.text_content for c in current_clips if c.text_content)
for existing in existing_videos:
if current_video_id and existing.id == current_video_id:
continue
@@ -1160,8 +1188,47 @@ class VideoDeduplicator:
# Issue #1702: 去掉 "frame_match_rate<0.3 整条跳过" 硬门槛——
# 局部片段复用帧比例天然低;coverage 为主指标,0 匹配自然得 0 分。
# duplicate_rate = 0.4 * frame_match_rate + 0.6 * temporal_coverage
dup_rate = (min(ev["frame_match_rate"], 1.0) * 0.4 + ev["temporal_coverage"] * 0.6) * 100
# 视觉维度:0.4 * frame_match_rate + 0.6 * temporal_coverage
visual_sim = min(ev["frame_match_rate"], 1.0) * 0.4 + ev["temporal_coverage"] * 0.6
# Issue #P2-后端3: 文案+结构维度
existing_plan_id = getattr(existing, "edit_plan_id", "") or ""
existing_clips_data = []
existing_text_content = ""
if existing_plan_id:
existing_clips = (
session.query(EditPlanClipModel)
.filter(EditPlanClipModel.plan_id == existing_plan_id)
.order_by(EditPlanClipModel.order)
.all()
)
existing_clips_data = [{"clip_type": c.clip_type, "duration": c.duration} for c in existing_clips]
existing_text_content = " ".join(c.text_content for c in existing_clips if c.text_content)
# 计算文案相似度(有文案才算)
text_sim = (
compute_text_similarity(current_text_content, existing_text_content)
if (current_text_content and existing_text_content)
else 0.0
)
# 计算结构相似度(有片段才算)
structure_sim = (
compute_structure_similarity(current_clips_data, existing_clips_data)
if (current_clips_data and existing_clips_data)
else 0.0
)
# 多维度融合:visual*0.5 + text*0.25 + structure*0.25
# 如果文案/结构数据缺失,只用视觉维度(visual 权重提升到 1.0)
if current_text_content and existing_text_content and current_clips_data and existing_clips_data:
dup_rate = (
visual_sim * VISUAL_WEIGHT + text_sim * TEXT_WEIGHT + structure_sim * STRUCTURE_WEIGHT
) * 100
else:
# 降级:只有视觉维度
dup_rate = visual_sim * 100
# 全片重复计数与 check_duplicate 判定口径一致
if ev["fusion"] >= DUPLICATE_THRESHOLD and (
@@ -1190,6 +1257,110 @@ class VideoDeduplicator:
}
# ── 文案 & 结构维度查重(Issue #P2-后端3) ────────────────────────
def _normalize_text(text: str) -> str:
"""文本标准化:去空白、转小写、去标点。"""
if not text:
return ""
# 去空白字符
text = re.sub(r"\s+", "", text)
# 转小写
text = text.lower()
# 去标点(只保留中文、字母、数字)
text = re.sub(r"[^\w\u4e00-\u9fff]", "", text)
return text
def compute_text_similarity(text1: str, text2: str) -> float:
"""计算两段文本的相似度(0~1)。
使用字符级 Jaccard 相似度:交集 / 并集。
适合短文本(配音脚本)的相似度比对。
Args:
text1: 第一段文本
text2: 第二段文本
Returns:
0~1 之间的相似度
"""
t1 = _normalize_text(text1)
t2 = _normalize_text(text2)
if not t1 and not t2:
return 1.0 # 都为空,视为完全相同
if not t1 or not t2:
return 0.0 # 一个为空,完全不同
# 字符级 Jaccard
set1 = set(t1)
set2 = set(t2)
intersection = set1 & set2
union = set1 | set2
if not union:
return 0.0
return len(intersection) / len(union)
def compute_structure_similarity(clips1: list[dict], clips2: list[dict]) -> float:
"""计算两个视频的结构相似度(0~1)。
结构维度包括:
1. 片段数差异(数量越接近越相似)
2. 片段类型序列(相同位置的片段类型是否一致)
3. 时长分布(各片段时长占比是否相似)
Args:
clips1: 第一个视频的片段列表,每项包含 {clip_type, duration}
clips2: 第二个视频的片段列表
Returns:
0~1 之间的相似度
"""
if not clips1 and not clips2:
return 1.0
if not clips1 or not clips2:
return 0.0
# 1. 片段数相似度(数量差异越大越低)
n1, n2 = len(clips1), len(clips2)
count_sim = min(n1, n2) / max(n1, n2)
# 2. 类型序列相似度(逐位比较,相同位置类型是否一致)
min_len = min(n1, n2)
type_matches = sum(1 for i in range(min_len) if clips1[i].get("clip_type") == clips2[i].get("clip_type"))
type_sim = type_matches / min_len if min_len > 0 else 0.0
# 3. 时长分布相似度(归一化后比较分布)
total1 = sum(c.get("duration", 0) for c in clips1)
total2 = sum(c.get("duration", 0) for c in clips2)
if total1 > 0 and total2 > 0:
# 归一化为占比
dist1 = [c.get("duration", 0) / total1 for c in clips1]
dist2 = [c.get("duration", 0) / total2 for c in clips2]
# 比较前 min_len 个片段的占比差异(L1 距离转相似度)
l1_dist = sum(abs(dist1[i] - dist2[i]) for i in range(min_len))
# 加上多出的片段占比
if n1 > n2:
l1_dist += sum(dist1[i] for i in range(n2, n1))
elif n2 > n1:
l1_dist += sum(dist2[i] for i in range(n1, n2))
# L1 距离范围 [0, 2],转为相似度 [0, 1]
duration_sim = 1.0 - (l1_dist / 2.0)
else:
duration_sim = 0.0
# 三维度加权:数量 0.3 + 类型 0.4 + 时长 0.3
return count_sim * 0.3 + type_sim * 0.4 + duration_sim * 0.3
def _save_fingerprint_chunks(
fingerprint: VideoFingerprint,
video_id: str,
+14 -4
View File
@@ -34,7 +34,9 @@ def create_video_record_and_dedup(
fps: float = 25.0,
name: str = "",
thumbnail_url: str = "",
) -> int:
) -> dict:
"""Returns: {"video_count": int, "is_duplicate": bool, "batch_similarity": float|None,
"duplicate_of": str|None} —— batch_similarity 为批次内最高相似度(无批次查重时 None)。"""
"""创建 GeneratedVideo 记录,计算指纹并执行查重(历史 + 批次)。
采用两阶段持久化:先计算所有指纹/查重数据(内存),
@@ -76,6 +78,7 @@ def create_video_record_and_dedup(
# ── Phase 2: 计算指纹 & 查重(全部在内存) ────────────────
deduplicator = VideoDeduplicator()
fingerprint = None
batch_similarity: float | None = None
try:
fingerprint = deduplicator.compute_fingerprint(video_path)
@@ -105,9 +108,11 @@ def create_video_record_and_dedup(
)
# (b) 批次内查重(仅当有 batch_id 时)
batch_similarity: float | None = None
if not duplicate_result and batch_id:
duplicate_result = deduplicator.check_batch_duplicate(fingerprint, batch_id, video_id, session)
if duplicate_result:
batch_similarity = float(duplicate_result.get("similarity", 0.0))
if duplicate_result:
generated_video.is_duplicate = True
generated_video.duplicate_of = duplicate_result["duplicate_of"]
@@ -161,7 +166,12 @@ def create_video_record_and_dedup(
generated_video.is_duplicate,
generated_video.duplicate_rate,
)
return 1
return {
"video_count": 1,
"is_duplicate": bool(generated_video.is_duplicate),
"batch_similarity": batch_similarity,
"duplicate_of": generated_video.duplicate_of,
}
except Exception as e:
logger.error(
"Failed to create video record / dedup for task %s: %s",
@@ -169,4 +179,4 @@ def create_video_record_and_dedup(
e,
)
session.rollback()
return 0
return {"video_count": 0, "is_duplicate": False, "batch_similarity": None, "duplicate_of": None}
+16 -4
View File
@@ -16,6 +16,9 @@ from typing import Any
from shared.ffmpeg_utils import ( # noqa: F401
DEFAULT_FFMPEG_TIMEOUT,
FFMPEG_BIN,
FFMPEG_ENCODE_CRF,
FFMPEG_ENCODE_PRESET,
FFMPEG_ENCODE_THREADS,
FFPROBE_BIN,
run_ffmpeg,
)
@@ -56,13 +59,18 @@ def build_xfade_filter_chain(
transitions: list[str],
*,
transition_duration: float = DEFAULT_TRANSITION_DURATION,
transition_durations: list[float] | None = None,
jitters: list[float] | None = None,
output_label: str = "outv",
) -> tuple[str, float]:
"""构建 xfade 转场滤镜链(re-export,#1766 增加逐转场时长与 jitter 支持)."""
return _build_xfade_filter_chain_base(
clip_durations,
clip_video_labels,
transitions,
transition_duration=transition_duration,
transition_durations=transition_durations,
jitters=jitters,
output_label=output_label,
)
@@ -291,9 +299,11 @@ def normalize_video(
"-c:v",
"libx264",
"-crf",
"23",
FFMPEG_ENCODE_CRF,
"-preset",
"medium",
FFMPEG_ENCODE_PRESET,
"-threads",
FFMPEG_ENCODE_THREADS,
"-c:a",
"aac",
"-b:a",
@@ -404,9 +414,11 @@ def random_edge_crop(
"-c:v",
"libx264",
"-preset",
"fast",
FFMPEG_ENCODE_PRESET,
"-crf",
"18",
FFMPEG_ENCODE_CRF,
"-threads",
FFMPEG_ENCODE_THREADS,
"-c:a",
"copy",
"-movflags",
+3 -2
View File
@@ -9,6 +9,7 @@ from dataclasses import dataclass
from typing import List
import ffmpeg
from shared.ffmpeg_utils import FFMPEG_ENCODE_CRF, FFMPEG_ENCODE_PRESET
logger = logging.getLogger(__name__)
@@ -90,8 +91,8 @@ class VideoProcessor:
acodec="aac",
s=f"{width}x{height}",
r=fps,
preset="medium",
crf=23,
preset=FFMPEG_ENCODE_PRESET,
crf=int(FFMPEG_ENCODE_CRF),
)
.overwrite_output()
.run(capture_stdout=True, capture_stderr=True)
@@ -599,7 +599,7 @@ class RenderAdapter:
# 抽帧天然带标题,因此这里传空字符串,避免 Pillow 二次叠加导致重影。
# Pillow 叠加仅用于 API 从源素材抽帧(源素材本身无标题)的兜底场景。
cover_candidates = extract_and_upload_cover_frames(
str(result.output_path), plan_id, task_id=job_id, num_frames=3, title_text=""
str(result.output_path), plan_id, task_id=job_id, num_frames=5, title_text=""
)
if cover_candidates:
logger.info(
+55 -13
View File
@@ -47,13 +47,23 @@ class RenderContext:
def clip_effective_duration(clip: ResolvedClip) -> float:
"""计算 clip 的有效时长.
"""计算 clip 的有效时长(#1749:目标段长始终为准)。
与 UnifiedRenderService._clip_effective_duration 逻辑一致。
与 UnifiedRenderService._clip_effective_duration 逻辑一致;素材短于段长的
部分由末帧冻结 tpad / 音频 apad 铺满,不在此处钳制。
"""
if clip.duration > 0:
return min(clip.duration, clip.actual_duration) if clip.actual_duration > 0 else clip.duration
return clip.actual_duration if clip.actual_duration > 0 else 0.0
return float(clip.duration)
return float(clip.actual_duration) if clip.actual_duration > 0 else 0.0
def _clip_freeze_seconds(clip: ResolvedClip) -> float:
"""读取 #1749 末帧冻结秒数(_resolve_clips 写入 config['_freeze_seconds'])。"""
cfg = getattr(clip, "config", None) or {}
try:
return max(0.0, float(cfg.get("_freeze_seconds", 0.0) or 0.0))
except (TypeError, ValueError):
return 0.0
def clip_has_audio(ctx: RenderContext, clip: ResolvedClip) -> bool:
@@ -273,6 +283,9 @@ def concat_main_audio(
if video_duration > 0 and (final_duration <= 0 or final_duration > video_duration):
final_duration = video_duration
# #1749 末帧冻结秒数(音频需 apad 补静音与视频等长)
freeze_seconds = _clip_freeze_seconds(clip)
# 音频倒放
reverse_config = ReverseConfig.from_dict(clip.config.get("reverse"))
has_reverse = reverse_config.enabled and reverse_config.reverse_audio
@@ -281,20 +294,28 @@ def concat_main_audio(
if not has_speed and not has_reverse:
# 无调速无倒放:根据是否需要裁剪/音量选择最高效的路径。
vol = _clip_volume(clip)
need_trim = trim_start > 0 or (effective_duration > 0 and final_duration < adjusted_duration)
need_trim = (
trim_start > 0 or freeze_seconds > 0 or (effective_duration > 0 and final_duration < adjusted_duration)
)
need_volume = abs(vol - 1.0) >= 1e-6
if need_trim:
# 需要裁剪:用 atrim 滤镜在滤镜链中精确裁剪(采样点级精度,不浪费解码)。
# 滤镜顺序:atrim → asetpts → volume(先裁剪再调音量,避免处理被丢弃的数据)。
# 滤镜顺序:atrim → asetpts → apad(冻结补静音) → volume。
af_parts: list[str] = []
if trim_start > 0 and effective_duration > 0:
af_parts.append(f"atrim=start={trim_start:.3f}:duration={final_duration:.3f}")
# 冻结场景:素材内可用时长 = 目标段长 − freeze(截掉超出素材的部分后补静音)
atrim_dur = final_duration
if freeze_seconds > 0:
atrim_dur = max(0.0, final_duration - freeze_seconds)
if trim_start > 0 and atrim_dur > 0:
af_parts.append(f"atrim=start={trim_start:.3f}:duration={atrim_dur:.3f}")
elif trim_start > 0:
af_parts.append(f"atrim=start={trim_start:.3f}")
elif final_duration > 0:
af_parts.append(f"atrim=duration={final_duration:.3f}")
elif atrim_dur > 0:
af_parts.append(f"atrim=duration={atrim_dur:.3f}")
af_parts.append("asetpts=PTS-STARTPTS")
if freeze_seconds > 0:
af_parts.append(f"apad=whole_dur={final_duration:.3f}")
if need_volume:
af_parts.append(f"volume={vol:.4f}")
command = [
@@ -334,8 +355,13 @@ def concat_main_audio(
"-ac",
"2",
]
af_simple: list[str] = []
if need_volume:
command.extend(["-af", f"volume={vol:.4f}"])
af_simple.append(f"volume={vol:.4f}")
if freeze_seconds > 0:
af_simple.append(f"apad=whole_dur={final_duration:.3f}")
if af_simple:
command.extend(["-af", ",".join(af_simple)])
if final_duration > 0:
command.extend(["-t", f"{final_duration:.3f}"])
command.append(str(output_path))
@@ -369,6 +395,10 @@ def concat_main_audio(
if abs(vol - 1.0) >= 1e-6:
audio_filters.append(f"volume={vol:.4f}")
# #1749 冻结补静音(调速后时长已变,apad 补齐到最终时长)
if freeze_seconds > 0:
audio_filters.append(f"apad=whole_dur={final_duration:.3f}")
# aformat 归一化:统一输出格式为 48000Hz + stereo + fltp
audio_filters.append("aformat=sample_rates=48000:channel_layouts=stereo:sample_fmts=fltp")
@@ -410,10 +440,13 @@ def concat_main_audio(
speed = getattr(clip, "playback_speed", 1.0) or 1.0
if not isinstance(speed, (int, float)) or speed <= 0:
speed = 1.0
# #1749 末帧冻结:素材内可用时长 = 目标段长 − freeze,apad 补静音
freeze_seconds = _clip_freeze_seconds(clip)
atrim_dur = max(0.0, effective_duration - freeze_seconds) if freeze_seconds > 0 else effective_duration
audio_filters: list[str] = []
if effective_duration > 0:
audio_filters.append(f"atrim=start={trim_start:.3f}:duration={effective_duration:.3f}")
audio_filters.append(f"atrim=start={trim_start:.3f}:duration={atrim_dur:.3f}")
audio_filters.append("asetpts=PTS-STARTPTS")
# 音频调速 — atempo 多级串联
@@ -435,6 +468,10 @@ def concat_main_audio(
if reverse_filter:
audio_filters.append(reverse_filter)
# #1749 冻结补静音:视频 tpad 延长后音频等长补齐(concat 时间轴对齐)
if freeze_seconds > 0:
audio_filters.append(f"apad=whole_dur={effective_duration:.3f}")
# 音量(0=静音,1=原声)
vol = _clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
@@ -506,12 +543,17 @@ def mix_with_independent_audio(
input_args.extend(["-i", str(clip.local_path)])
effective_duration = clip_effective_duration(clip)
trim_start = getattr(clip, "start_time", 0) or 0
freeze_seconds = _clip_freeze_seconds(clip)
atrim_dur = max(0.0, effective_duration - freeze_seconds) if freeze_seconds > 0 else effective_duration
clip_filters = []
if effective_duration > 0:
clip_filters.append(f"atrim=start={trim_start:.3f}:duration={effective_duration:.3f}")
clip_filters.append(f"atrim=start={trim_start:.3f}:duration={atrim_dur:.3f}")
clip_filters.append("asetpts=PTS-STARTPTS")
else:
clip_filters.append("asetpts=PTS-STARTPTS")
# #1749 冻结补静音(与视频 tpad 等长)
if freeze_seconds > 0:
clip_filters.append(f"apad=whole_dur={effective_duration:.3f}")
vol = _clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
clip_filters.append(f"volume={vol:.4f}")
@@ -279,21 +279,25 @@ def extract_and_upload_cover_frames(
plan_id: str,
*,
task_id: str = "",
num_frames: int = 3,
num_frames: int = 5, # 抽 5 帧候选,通过质量评分选出最佳帧
title_text: str = "",
title_color: str = "#ffffff",
title_position: str = "bottom",
title_font_size: int | None = None,
) -> list[dict]:
"""从视频中抽取多帧作为封面候选,上传到 OSS。
"""从视频中抽取多帧作为封面候选,通过质量评分选出最佳帧,上传到 OSS。
优先使用 MediaKit 智能抽帧,失败时降级到 ffmpeg 直接抽帧。
流程:
1. 优先使用 MediaKit 智能抽帧(多抽一些供选择)
2. MediaKit 不足时降级到 ffmpeg 均匀抽帧
3. 对所有候选帧进行质量评分(清晰度/亮度/色彩丰富度)
4. 按分数从高到低排序返回
Args:
video_path: 视频文件路径
plan_id: 编辑计划 ID(用于生成 storage key)
task_id: 任务 ID(用于生成独立的 storage key,避免标题变更时封面冲突)
num_frames: 抽取帧数(默认 3)
num_frames: 抽取候选帧数(默认 5,通过质量评分选出最佳帧)
title_text: 标题文字;非空时用 Pillow 叠加到每帧。
从已渲染视频抽帧时通常传空(标题已烧录);从源素材抽帧时传标题。
title_color: 标题字体颜色(#RRGGBB)
@@ -301,7 +305,7 @@ def extract_and_upload_cover_frames(
title_font_size: 标题字号,None 时自动计算
Returns:
封面候选列表,每项包含 {"url": str, "position": float}
封面候选列表(按质量分数降序),每项包含 {"url": str, "position": float, "score": float}
"""
import httpx
from video_processing.ffmpeg_utils import probe_duration
@@ -313,80 +317,125 @@ def extract_and_upload_cover_frames(
duration = 0.0
candidates: list[dict] = []
_temp_paths: list[str] = [] # 收集所有临时文件路径,最后统一清理
# 优先尝试 MediaKit 智能抽帧
mediakit_frames = _extract_frames_via_mediakit(video_path, plan_id, num_frames)
if mediakit_frames:
for i, frame in enumerate(mediakit_frames):
frame_url = frame.get("image_url")
if not frame_url:
continue
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
try:
# 下载 MediaKit 返回的帧图
resp = httpx.get(frame_url, timeout=30, follow_redirects=True)
resp.raise_for_status()
with open(tmp.name, "wb") as f:
f.write(resp.content)
try:
# ── 阶段 1:抽帧 ──────────────────────────────────────────────
# 优先尝试 MediaKit 智能抽帧
mediakit_frames = _extract_frames_via_mediakit(video_path, plan_id, num_frames)
if mediakit_frames:
for i, frame in enumerate(mediakit_frames):
frame_url = frame.get("image_url")
if not frame_url:
continue
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
_temp_paths.append(tmp.name)
try:
# 下载 MediaKit 返回的帧图
resp = httpx.get(frame_url, timeout=30, follow_redirects=True)
resp.raise_for_status()
with open(tmp.name, "wb") as f:
f.write(resp.content)
# 叠加标题文字(如需要)
if title_text and title_text.strip():
apply_title_overlay(
tmp.name,
title_text,
color=title_color,
position=title_position,
font_size=title_font_size,
# 叠加标题文字(如需要)
if title_text and title_text.strip():
apply_title_overlay(
tmp.name,
title_text,
color=title_color,
position=title_position,
font_size=title_font_size,
)
storage_key = f"covers/{plan_id}/{task_id}/mediakit_frame_{i}.jpg"
url = upload_to_oss(tmp.name, storage_key)
if url:
seek_time = frame.get("timestamp", 0.0)
candidates.append(
{
"url": url,
"position": round(seek_time, 2),
"image_path": tmp.name,
}
)
except Exception as e:
logger.warning("[thumbnail] MediaKit 帧 %d 处理失败: %s", i, e)
if len(candidates) >= num_frames:
logger.info("[thumbnail] MediaKit 智能抽帧完成: %d 帧", len(candidates))
else:
logger.warning("[thumbnail] MediaKit 抽帧不足 %d 帧,降级到 ffmpeg", num_frames)
# Fallback: ffmpeg 直接抽帧(仅当 MediaKit 不足时)
if len(candidates) < num_frames:
logger.info("[thumbnail] 使用 ffmpeg 抽帧补充")
# 均匀分布抽帧点:从 10% 到 90%
for i in range(num_frames):
ratio = 0.1 + 0.8 * i / max(num_frames - 1, 1)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
_temp_paths.append(tmp.name)
try:
frame_path = extract_first_frame(
video_path,
output_path=tmp.name,
seek_ratio=ratio,
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}/{task_id}/frame_{i}.jpg"
url = upload_to_oss(frame_path, storage_key)
if url:
seek_time = max(0.5, duration * ratio) if duration > 0 else 0.0
candidates.append(
{
"url": url,
"position": round(seek_time, 2),
"image_path": tmp.name,
}
)
except Exception as e:
logger.warning("[thumbnail] 封面候选帧 %d 提取失败: %s", i, e)
storage_key = f"covers/{plan_id}/{task_id}/mediakit_frame_{i}.jpg"
url = upload_to_oss(tmp.name, storage_key)
if url:
seek_time = frame.get("timestamp", 0.0)
candidates.append({"url": url, "position": round(seek_time, 2)})
except Exception as e:
logger.warning("[thumbnail] MediaKit 帧 %d 处理失败: %s", i, e)
finally:
Path(tmp.name).unlink(missing_ok=True)
# ── 阶段 2:质量评分 ────────────────────────────────────────────
if len(candidates) > 1:
try:
from packages.shared.cover_frame_scorer import score_frames
if len(candidates) >= num_frames:
logger.info("[thumbnail] MediaKit 智能抽帧完成: %d 帧", len(candidates))
return candidates[:num_frames]
logger.warning("[thumbnail] MediaKit 抽帧不足 %d 帧,降级到 ffmpeg", num_frames)
# Fallback: ffmpeg 直接抽帧
logger.info("[thumbnail] 使用 ffmpeg 抽帧")
# 均匀分布抽帧点:从 10% 到 90%
for i in range(num_frames):
ratio = 0.1 + 0.8 * i / max(num_frames - 1, 1)
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
tmp.close()
try:
frame_path = extract_first_frame(
video_path,
output_path=tmp.name,
seek_ratio=ratio,
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,
candidates = score_frames(candidates)
logger.info(
"[thumbnail] 封面帧质量评分完成: plan_id=%s count=%d best_score=%.1f",
plan_id,
len(candidates),
candidates[0].get("score", 0.0) if candidates else 0.0,
)
except Exception:
logger.warning(
"[thumbnail] 封面帧质量评分失败,保持原始顺序: plan_id=%s",
plan_id,
exc_info=True,
)
storage_key = f"covers/{plan_id}/{task_id}/frame_{i}.jpg"
url = upload_to_oss(frame_path, storage_key)
if url:
seek_time = max(0.5, duration * ratio) if duration > 0 else 0.0
candidates.append({"url": url, "position": round(seek_time, 2)})
except Exception as e:
logger.warning("[thumbnail] 封面候选帧 %d 提取失败: %s", i, e)
finally:
Path(tmp.name).unlink(missing_ok=True)
return candidates
# ── 阶段 3:清理临时文件 ────────────────────────────────────────
# 移除 image_path(不再需要),但临时文件统一清理
for c in candidates:
c.pop("image_path", None)
return candidates
finally:
# 统一清理所有临时文件
for path in _temp_paths:
try:
Path(path).unlink(missing_ok=True)
except Exception:
pass

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