Compare commits

...

25 Commits

Author SHA1 Message Date
xiaoxia 9709d5471b fix(ass): Fontsize乘1.35补偿系数对齐CSS预览字号 (#1516)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m56s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 6m37s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 6m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 6m55s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m56s
AI Code Review / AI Code Review (pull_request) Failing after 4m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 8m58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Failing after 11m6s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 12m56s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m2s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m54s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 13m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m46s
CI/CD Pipeline / Unit Tests (push) Successful in 16m46s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 8m9s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 8m30s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m55s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 29s
2026-08-27 01:44:19 +08:00
xiaoxia e1482a4b11 fix: Step2素材选择后调用/clips/from-assets,修复片段数量不足+随机起始时间 (#1515)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 4m9s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 5m10s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5m22s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 5m52s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m28s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 7m32s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 7m47s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m48s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m26s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 24s
问题:前端在本地构建clips(每素材1个片段,start_time=0),
未调用后端/clips/from-assets,导致模板要求4个片段但只选3个素材时
只生成3个片段,后端随机start_time和多片段逻辑从未被触发。

修复:
- createClipsFromAssets增加requiredClipsCount可选参数
- useStep2Materials改为先清空旧片段再调用from-assets,
  required_clips_count=模板segments数
- 删除废弃的buildClipsFromAssets工具函数
2026-08-27 00:42:32 +08:00
xiaoxia d4c8064cdc fix(fonts): FONT_NAME_MAP改为Noto Sans SC适配容器实际字体 (#1514)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m39s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 5m50s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m35s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m24s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 5m9s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m30s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m41s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m15s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m22s
CI/CD Pipeline / Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m17s
CI/CD Pipeline / Unit Tests (push) Successful in 13m18s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
2026-08-27 00:35:48 +08:00
xiaoxia 9178e06bb3 fix: 恢复步骤4-6实时预览,步骤7显示最终视频,步骤1-3右侧留空 (#1513)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m12s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 6m1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 6m20s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m41s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 7m21s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 7m25s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m30s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m20s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m22s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m52s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m3s
AI Code Review / AI Code Review (pull_request) Successful in 8m35s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m44s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m55s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 15m11s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 15m38s
CI/CD Pipeline / Unit Tests (push) Successful in 17m41s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m19s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 7m53s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 8m6s
CI/CD Pipeline / CI Gate (pull_request) Successful in 25s
2026-08-26 23:37:00 +08:00
xiaoxia 2c1af458b9 feat(editor): from-assets支持同素材多片段+随机起始时间+去重+required_clips_count (#1512)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m42s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m46s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m37s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m23s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m56s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m56s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m57s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 13m10s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 13m12s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 8m45s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 10m3s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 23m58s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 23m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 25m5s
CI/CD Pipeline / Unit Tests (push) Successful in 26m28s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 20m11s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m3s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m54s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m50s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 10m57s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 10m55s
CI/CD Pipeline / CI Gate (pull_request) Successful in 26s
2026-08-26 22:40:02 +08:00
xiaoxia 004ccb1af2 fix: 移除预览播放器,改为内联播放生成视频;删除生成数量功能 (#1511)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2m32s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m22s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 35s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 8m48s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 9m20s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m29s
AI Code Review / AI Code Review (pull_request) Failing after 9m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 10m8s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 10m10s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m50s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m26s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 18m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m43s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 19m37s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m21s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m58s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m6s
CI/CD Pipeline / Unit Tests (push) Successful in 19m6s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 10m55s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 11m17s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 19m24s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 40s
- 移除 FrontendPreviewPlayer 实时预览组件和视频预览弹窗 Modal
- 生成的视频在右侧内联播放(HTML5 video,controls + autoPlay)
- 添加生成中/失败/空状态的 UI 展示
- 删除「生成数量」(generateCount)全链路代码
- 清理 previewVideo/previewModalOpen 状态和 TTS 预览音频死代码
- 添加 .xx-inline-video-player 样式
2026-08-26 21:21:05 +08:00
xiaoxia 5482d3c58d chore: 触发 base image 重建(ACR 推送重试)
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4m9s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m43s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m32s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 8m26s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 8m25s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 8m54s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m25s
AI Code Review / AI Code Review (pull_request) Failing after 10m7s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m57s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m34s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 17m10s
CI/CD Pipeline / Build Staging API Image (push) Successful in 24m29s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
API Base Image Build / Build API Base Image (push) Successful in 45m16s
Worker Base Image Build / Build Worker Base Image (push) Successful in 57m33s
2026-08-26 20:51:37 +08:00
xiaoxia a271c7981f chore: 重新触发 base image 构建(上次 ACR 推送超时)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-26 20:50:51 +08:00
xiaoxia 989f6046d8 fix: 字体文件改为仓库内预置,不再从 GitHub 下载
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3m44s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m19s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m34s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 6m36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 6m50s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 7m29s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 7m31s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 4m13s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m52s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m11s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m3s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m13s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m26s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m28s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m8s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 14m41s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 15m14s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m32s
CI/CD Pipeline / Unit Tests (push) Successful in 14m54s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m51s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 6m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m26s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 24s
API Base Image Build / Build API Base Image (push) Successful in 43m5s
Worker Base Image Build / Build Worker Base Image (push) Failing after 42m1s
- 将 NotoSansSC-VF.ttf(17MB)提交到 infra/fonts/
- Dockerfile 改为 COPY 方式安装字体,彻底解决国内构建环境
  无法稳定访问 GitHub 导致 base image 构建失败/超时的问题
- 同时修复 api-base 和 worker-base 两个 Dockerfile
2026-08-26 20:06:32 +08:00
xiaoxia 961aa4fe34 fix: Worker Base Image 构建重试 + GitHub 备用镜像
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m41s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m9s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m14s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m36s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m47s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 2m30s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m8s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m38s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m19s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m12s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m40s
CI/CD Pipeline / Integration Tests (push) Successful in 3m49s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m55s
CI/CD Pipeline / Unit Tests (push) Successful in 13m59s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m15s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 24m2s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m27s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 47s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m11s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m0s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
API Base Image Build / Build API Base Image (push) Successful in 38m50s
Worker Base Image Build / Build Worker Base Image (push) Successful in 1h15m55s
- curl 添加 --retry 3 --connect-timeout 30
- 主 URL 失败时自动回退到 ghfast.top 代理镜像
- 同时修复 api-base 和 worker-base 两个 Dockerfile
2026-08-26 19:34:40 +08:00
xiaoxia 3b0a29d850 fix: 修复 Noto Sans CJK SC Bold 字体混入 Mono 等宽变体问题 (#1510)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m4s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m52s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m17s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m39s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 33s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
AI Code Review / AI Code Review (pull_request) Successful in 3m54s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m18s
Worker Base Image Build / Build Worker Base Image (push) Failing after 8m49s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m40s
CI/CD Pipeline / Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m59s
CI/CD Pipeline / Unit Tests (push) Successful in 15m15s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m14s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
API Base Image Build / Build API Base Image (push) Successful in 19m55s
2026-08-26 19:11:59 +08:00
xiaoxia 7da62f0f82 fix: 移除 asset_durations 的 random_preview 条件限制 (#1509)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m54s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m28s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m35s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m16s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m51s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m49s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 37s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m37s
AI Code Review / AI Code Review (pull_request) Successful in 4m7s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m31s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m29s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m5s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m58s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m22s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m59s
CI/CD Pipeline / Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Unit Tests (push) Successful in 15m5s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m6s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-26 18:46:37 +08:00
CI Bot b2d2abf8b7 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m7s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m12s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m47s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m48s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m26s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m30s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-08-26 10:42:39 +00:00
xiaoxia 57f16364f1 fix: 修复 CI 单测隔离问题 + ruff 代码质量违规
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 57s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m15s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m8s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m25s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m38s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m58s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m56s
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- test_generated_video_creation_logic: 添加 teardown_class 恢复 setup_class
  中替换的 sys.modules 条目(video_processing.thumbnail_generator / dedup),
  避免 MagicMock 污染后续测试导致 4 个单测失败
- plan_generator_utils: 循环变量 seg_start 未使用 → _seg_start(B007)
- acr_cleanup: 移除未使用变量 open_pr_shas(F841)
- auto_fix_formatting: 移除未使用变量 head_branch_tmp / fix_mode(F841)
2026-08-26 18:36:37 +08:00
CI Bot 850559505f style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m4s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m19s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 52s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 54s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m11s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m14s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m0s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 3m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m15s
AI Code Review / AI Code Review (pull_request) Successful in 8m52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m24s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 13m17s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
2026-08-26 09:28:53 +00:00
xiaoxia ecbdd49dc1 feat: 素材片段随机起始时间+去重 + MediaKit智能封面抽帧
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m13s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m14s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m24s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m55s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / 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
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 54s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m21s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 5m1s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m53s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m12s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 52s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m23s
CI/CD Pipeline / Integration Tests (push) Successful in 4m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m20s
CI/CD Pipeline / Unit Tests (push) Failing after 12m24s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
## 任务1: 素材片段随机起始时间
- 修改 _calc_random_start_time 增加 used_segments 参数,避开已使用时间段
- 四个分配函数(ONE_TAKE/PIP/VOICE_OVER/VOICE_PIP)各自维护 used_segments
- 同一素材被多个clip使用时,自动记录已用区间,下次随机时避开
- 如果无空间可用,回退到最后已用段之后或缩短时长

## 任务2: MediaKit智能封面抽帧
- 新增 _extract_frames_via_mediakit 函数,上传视频到OSS后调用MediaKit API
- 使用 SceneChange 策略智能选取关键帧
- 失败时自动降级到 ffmpeg 均匀抽帧
- 抽帧后下载帧图、可选叠加标题、上传OSS

## 测试
- 新增 8 个 _calc_random_start_time 单测
- 新增 4 个 distribute_assets 去重逻辑单测
- 新增 4 个 MediaKit 集成单测
- 修复 3 个因字体映射变更的测试断言
2026-08-26 17:22:08 +08:00
xiaoxia 23b6f23bad fix: ASS Fontname 字体名称映射到服务器注册名 (#1508)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 56s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m30s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m42s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m1s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m54s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m8s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m34s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 4m8s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m0s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m20s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m49s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m14s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m17s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m19s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m3s
CI/CD Pipeline / Unit Tests (push) Failing after 14m3s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Failing after 12m14s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m10s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m18s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-26 16:56:01 +08:00
xiaoxia bd0082b764 fix: 标题行高1.3→1.05,与后端ASS渲染行间距对齐 (#1507)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m9s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m1s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m16s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m6s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m4s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m55s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m26s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m40s
AI Code Review / AI Code Review (pull_request) Successful in 4m34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 55s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m40s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m41s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m31s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 53s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Integration Tests (push) Successful in 3m57s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m19s
CI/CD Pipeline / Unit Tests (push) Successful in 14m11s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m43s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
2026-08-26 12:42:34 +08:00
xiaoxia 32990194d4 fix: 标题顶部边距60→120,标题位置下移 (#1506)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 44s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m18s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m50s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m59s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 4m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 40s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m2s
AI Code Review / AI Code Review (pull_request) Failing after 4m45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m32s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m40s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m48s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 56s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m6s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m57s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 10m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 14m25s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m31s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m29s
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-26 01:16:00 +08:00
xiaoxia 11dde783b9 fix: 标题顶部边距60→120,标题位置下移 (#1506)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m14s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 31s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m17s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m58s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m42s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m14s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 2m38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m59s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m6s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 26s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 7m33s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 9m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 44s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m40s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m23s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m26s
CI/CD Pipeline / Integration Tests (push) Successful in 4m10s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Unit Tests (push) Successful in 14m44s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m31s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 6s
2026-08-26 00:46:53 +08:00
xiaoxia ddb1a3544d fix: 混音主视频clip音量滤镜缺失 + 自动换行识别已有\N标记 (#1505)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1m16s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m45s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m20s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m33s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m5s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 4m53s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 35s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m16s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m56s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m8s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m19s
CI/CD Pipeline / Integration Tests (push) Successful in 3m7s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m25s
CI/CD Pipeline / Unit Tests (push) Successful in 13m8s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m21s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-26 00:21:00 +08:00
xiaoxia 323bd2da5e fix: 标题/换行 + TTS replace模式静音原音 + 配音素材库静音原音 (#1504)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 46s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m52s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m7s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 5m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m38s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m56s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
AI Code Review / AI Code Review (pull_request) Successful in 5m25s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m16s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m19s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m9s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 9m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m40s
CI/CD Pipeline / Integration Tests (push) Successful in 4m40s
CI/CD Pipeline / Unit Tests (push) Successful in 14m37s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m58s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m59s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 7s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 23:06:00 +08:00
xiaoxia 058bfac5c2 fix: 字号滑块上限从48提升至128 (#1503)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 37s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m57s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 55s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m4s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m22s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m30s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m46s
AI Code Review / AI Code Review (pull_request) Failing after 4m37s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 36s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m53s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 12m0s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m13s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 13m50s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m17s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m58s
CI/CD Pipeline / Unit Tests (push) Successful in 17m29s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 6m27s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 17m13s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
2026-08-25 22:23:39 +08:00
xiaoxia 7635a20fdb fix(ass): 字段名归一化(font_size/font_color→size/color) + 移除字号上限36 (#1501)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 47s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m8s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m23s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m58s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m4s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m55s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m47s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m40s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m47s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 45s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m6s
AI Code Review / AI Code Review (pull_request) Failing after 7m46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m1s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m58s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 12m36s
CI/CD Pipeline / Unit Tests (push) Successful in 14m28s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 11m53s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 5m11s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m0s
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-25 21:56:02 +08:00
xiaoxia be88e49543 fix: 预览标题坐标系与后端 ASS 烧录 1:1 对齐 (#1502)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 41s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m51s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m56s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m10s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m9s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 2m38s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 2m49s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 2m29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 55s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m8s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 54s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 9m26s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 10m56s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m5s
CI/CD Pipeline / Unit Tests (push) Successful in 12m51s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 12m51s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 13m28s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
CI/CD Pipeline / Integration Tests (push) Successful in 13m5s
CI/CD Pipeline / CI Gate (push) Has been skipped
- 根据 videoRatio 动态选择后端实际分辨率(9:16→1080x1920, 16:9→1920x1080, 1:1→1080x1080)
- 字号缩放分母从 720 改为实际 PlayResY,移除 96 上限
- 水平/垂直边距按实际 PlayResX/PlayResY 计算
- 描边/阴影按容器缩放比例同步放大
- FrontendPreviewPlayer 标题同时支持半角/和全角/换行
2026-08-25 21:22:14 +08:00
40 changed files with 1759 additions and 432 deletions
@@ -25,6 +25,7 @@ from app.services.edit_template_service import EditTemplateService
from fastapi import APIRouter, Depends, HTTPException, Query, status
from packages.adapters.sqlalchemy_impl.asset_repository import SQLAlchemyAssetRepository
from packages.domain.plan_generator_utils import _calc_random_start_time
from .dependencies import get_draft_plan_id, get_editor_services
from .schemas import (
@@ -45,6 +46,9 @@ from .schemas import (
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Template Editor"])
# 编辑器默认片段时长(秒)
_DEFAULT_EDITOR_CLIP_DURATION = 5.0
def _clip_to_response(clip, asset_url: str | None = None) -> EditorClipResponse:
"""统一构造片段响应 — 与 edit_plan_clips 表字段完全对齐"""
@@ -155,10 +159,7 @@ def list_draft_clips(
url_map = _build_asset_url_map(asset_ids, asset_repo)
return EditorClipListResponse(
items=[
_clip_to_response(c, asset_url=url_map.get(getattr(c, "asset_id", "") or ""))
for c in clips
],
items=[_clip_to_response(c, asset_url=url_map.get(getattr(c, "asset_id", "") or "")) for c in clips],
total=total,
)
@@ -272,9 +273,7 @@ def split_draft_clip(
try:
result = plan_svc.split_clip(clip_id, body.split_time)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)
) from exc
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
left = result["left_clip"]
right = result["right_clip"]
asset_ids = [getattr(left, "asset_id", "") or "", getattr(right, "asset_id", "") or ""]
@@ -304,9 +303,7 @@ def merge_draft_clips(
try:
merged = plan_svc.merge_clips(body.clip_ids)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)
) from exc
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
asset_id = getattr(merged, "asset_id", "") or ""
url_map = _build_asset_url_map([asset_id], asset_repo) if asset_id else {}
return {
@@ -360,28 +357,90 @@ def create_clips_from_assets_editor(
body: ClipsFromAssetsRequest,
plan_id: str = Depends(get_draft_plan_id),
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
asset_repo: SQLAlchemyAssetRepository = Depends(get_asset_repository),
current_user: AuthenticatedUser = Depends(get_current_user),
) -> ClipsFromAssetsResponse:
"""从素材批量创建片段"""
"""从素材批量创建片段(支持同一素材切多个片段 + 随机起始时间 + 去重).
逻辑:
1. 模板要求 N 个片段,必须创建 N 个(不管素材有几个)
2. 素材数量 < 片段数量时,同一素材轮询切多个片段
3. 每个片段从素材中随机选取不重复时间段
4. 素材时长不足 5s 时缩短 clip duration
5. 新片段追加到时间线末尾(order 在现有最大值基础上递增)
"""
_, plan_svc = services
required_count = body.required_clips_count if body.required_clips_count is not None else len(body.asset_ids)
# 去重后批量获取素材实际时长,避免重复查询
unique_asset_ids = list(dict.fromkeys(body.asset_ids))
asset_durations: dict[str, float] = {}
for asset_id in unique_asset_ids:
asset = asset_repo.get(asset_id)
if asset and hasattr(asset, "duration"):
asset_durations[asset_id] = float(asset.duration or 0.0)
# 计算追加起始 order:当前 plan 已有片段的最大 order + 1
# list_clips 返回 List[EditPlanClip]
existing_clips_list = plan_svc.list_clips(plan_id)
next_order = max((c.order for c in existing_clips_list), default=-1) + 1
used_segments: dict[str, list[tuple[float, float]]] = {}
clips = []
for i, asset_id in enumerate(body.asset_ids):
for i in range(required_count):
# 轮询分配素材:素材不够时同一素材切多个片段
asset_id = body.asset_ids[i % len(body.asset_ids)]
# 素材时长不足时缩短 clip duration
asset_total = asset_durations.get(asset_id, 0.0)
if asset_total > 0:
clip_duration = min(_DEFAULT_EDITOR_CLIP_DURATION, asset_total)
else:
clip_duration = _DEFAULT_EDITOR_CLIP_DURATION
# 计算随机 start_time,避开已使用的时间段
start_time = _calc_random_start_time(asset_id, clip_duration, asset_durations, used_segments)
if start_time is None:
# 素材时长信息缺失,无法计算随机起始时间
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"素材 {asset_id} 时长信息缺失,无法创建片段",
)
# 记录已使用时间段(用于去重)
used_segments.setdefault(asset_id, []).append((start_time, start_time + clip_duration))
try:
clip = plan_svc.create_clip(
plan_id,
clip_type="main",
order=body.start_order + i if hasattr(body, "start_order") else i,
duration=5.0,
clip_type=body.clip_type or "main",
order=next_order + i,
duration=clip_duration,
start_time=start_time,
asset_id=asset_id,
)
clips.append(clip)
except ValueError:
pass
except ValueError as exc:
logger.error(
"创建片段失败: plan_id=%s asset_id=%s order=%d error=%s",
plan_id,
asset_id,
next_order + i,
exc,
exc_info=True,
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"创建片段失败: {exc}",
) from exc
logger.info(
"模板编辑器从素材创建片段: template_id=%s plan_id=%s count=%d by user=%s",
"模板编辑器从素材创建片段: template_id=%s plan_id=%s required=%d actual=%d by user=%s",
template_id,
plan_id,
required_count,
len(clips),
current_user.user.id,
)
@@ -167,6 +167,7 @@ class ClipsFromAssetsRequest(BaseModel):
asset_ids: List[str] = Field(..., min_length=1, max_length=200, description="素材 ID 列表,按顺序追加到时间线末尾")
clip_type: str = Field(default="main", description="片段类型,默认 main")
required_clips_count: Optional[int] = Field(default=None, ge=1, le=200, description="要求创建的片段数量;不传则等于素材数量")
class ClipsFromAssetsResponse(BaseModel):
@@ -118,9 +118,9 @@ class PlanGeneratorService:
# 4. 按 editing_mode 分配素材
if asset_ids:
# 如果是随机预览模式,获取素材时长信息
# 获取素材时长信息,用于随机起始时间
asset_durations = None
if random_preview and self._asset_repo:
if self._asset_repo:
asset_durations = self._fetch_asset_durations(asset_ids)
self._distribute_assets(
clips,
+9 -1
View File
@@ -90,10 +90,18 @@ export async function createClipsFromAssets(
templateId: string,
assetIds: string[],
clipType = "main",
requiredClipsCount?: number,
): Promise<ClipsFromAssetsResponse> {
const body: Record<string, unknown> = {
asset_ids: assetIds,
clip_type: clipType,
}
if (requiredClipsCount !== undefined) {
body.required_clips_count = requiredClipsCount
}
const response = await apiClient.post<ClipsFromAssetsResponse>(
`/templates/${templateId}/editor/clips/from-assets`,
{ asset_ids: assetIds, clip_type: clipType },
body,
)
return response.data
}
@@ -114,8 +114,6 @@ export interface EditPlanConfig {
auto_subtitles?: boolean
/** 是否启用 BGM */
bgm?: boolean
/** 生成数量 */
generate_count?: number
/** 素材模式 */
material_mode?: string
/** 前端标题设置(Step4 自动保存,与 title_config 字段分离,不影响后端渲染) */
+33 -63
View File
@@ -4,12 +4,12 @@
* 左右布局:左侧 generate-form + 右侧 generate-preview
*
* 架构:
* - Step4+ 右侧预览面板使用 FrontendPreviewPlayer 实时播放素材片段
* - 标题样式编辑时 CSS 层实时叠加预览,所见即所得
* - 步骤 4-6 右侧显示 FrontendPreviewPlayer 实时预览
* - 步骤 7 右侧内联播放生成的最终视频
* - 点"确认生成"时调用 createGenerationTask 创建一次服务器渲染任务
*/
import React, { useMemo, useState, useEffect, useRef } from "react"
import { Modal, message } from "antd"
import { message } from "antd"
import { useNavigate } from "react-router-dom"
import type { VoiceClone } from "@/api/voice-clone"
import { useQuery } from "@tanstack/react-query"
@@ -20,9 +20,8 @@ import {
calculateTotalVideoDuration,
estimateTotalVideoDuration,
} from "./utils/calculateTotalVideoDuration"
import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateResultPanel from "./components/GenerateResultPanel"
import FrontendPreviewPlayer from "./components/FrontendPreviewPlayer"
import GenerateStepsBar from "./components/GenerateStepsBar"
import GenerateStepContent from "./components/GenerateStepContent"
import GenerateStepActions from "./components/GenerateStepActions"
import { useGenerateFormState } from "./hooks/useGenerateFormState"
@@ -64,8 +63,6 @@ const GeneratePage: React.FC = () => {
presetVoices,
cloneModalOpen,
setCloneModalOpen,
generateCount,
setGenerateCount,
videoRatio,
duration,
style,
@@ -73,10 +70,6 @@ const GeneratePage: React.FC = () => {
bgm,
editPlanId,
sourceEditPlanId,
previewVideo,
setPreviewVideo,
previewModalOpen,
setPreviewModalOpen,
previewTaskId,
setPreviewTaskId,
storedSourceEditPlanId,
@@ -89,7 +82,7 @@ const GeneratePage: React.FC = () => {
onTitleSettingsChange: setTitleSettings,
})
/* ── 配音预览音频TTS 试听)── */
/* ── 配音素材库TTS 试听)── */
const { data: voiceMaterials = [] } = useQuery({
queryKey: ["assets", "voice"],
queryFn: () => getAssetsByKind("voice", { limit: 50 }),
@@ -99,30 +92,24 @@ const GeneratePage: React.FC = () => {
const ttsAbortRef = useRef<AbortController | null>(null)
useEffect(() => {
// 如果 selectedVoice 是已上传的配音素材,直接用 file_url
const voiceAsset = voiceMaterials.find((m) => m.id === selectedVoice)
if (voiceAsset?.file_url) {
setPreviewVoiceAudioUrl(voiceAsset.file_url)
return
}
// 没有选中的 voice 或标题,跳过
const voiceId = selectedClonedVoice || selectedVoice
if (!voiceId || !titleSettings.title) {
setPreviewVoiceAudioUrl(null)
return
}
// 预设音色 / 克隆音色 → 调 TTS 合成
ttsAbortRef.current?.abort()
const controller = new AbortController()
ttsAbortRef.current = controller
let cancelled = false
previewTts({
text: titleSettings.title,
voice_id: voiceId,
})
previewTts({ text: titleSettings.title, voice_id: voiceId })
.then((res) => {
if (!cancelled && res.audio_url) {
setPreviewVoiceAudioUrl(res.audio_url)
@@ -221,7 +208,6 @@ const GeneratePage: React.FC = () => {
duration,
autoSubtitles,
bgm,
generateCount,
sourceEditPlanId: storedSourceEditPlanId || sourceEditPlanId,
previewTaskId,
bgmConfig,
@@ -241,7 +227,7 @@ const GeneratePage: React.FC = () => {
<GenerateStepsBar currentStep={currentStep} onStepClick={setCurrentStep} />
<div className="xx-generate-layout">
<div className={`xx-generate-layout${currentStep < 4 ? " full-width" : ""}`}>
{/* ════ 左侧:表单区 ════ */}
<div className="xx-generate-form">
<GenerateStepContent
@@ -286,8 +272,6 @@ const GeneratePage: React.FC = () => {
hasProcessing={hasProcessing}
cloneModalOpen={cloneModalOpen}
onCloneModalOpenChange={setCloneModalOpen}
generateCount={generateCount}
onGenerateCountChange={setGenerateCount}
generating={generating}
generated={generated}
generateError={generateError}
@@ -309,9 +293,9 @@ const GeneratePage: React.FC = () => {
/>
</div>
{/* ════ 右侧:预览 + 结果 ════ */}
{/* ════ 右侧:步骤 4-6 实时预览,步骤 7 最终视频 ════ */}
<div className="xx-generate-right-col">
{currentStep >= 4 && !!currentTemplate && (
{currentStep >= 4 && currentStep <= 6 && !!currentTemplate && (
<FrontendPreviewPlayer
assets={previewAssets}
template={currentTemplate}
@@ -331,48 +315,34 @@ const GeneratePage: React.FC = () => {
}}
/>
)}
{currentStep >= 6 && (
<GenerateResultPanel
generated={generated}
generating={generating}
progress={progress}
generateError={generateError}
generatedVideos={generatedVideos}
onVideoPreview={(video) => {
setPreviewVideo(video)
setPreviewModalOpen(true)
}}
onDownload={handleDownload}
onShare={handleShare}
onGoToLibrary={() => navigate("/app/products")}
/>
{currentStep === 7 && generated && generatedVideos.length > 0 && (
<div className="xx-inline-video-player">
<video
src={generatedVideos[0].download_url || generatedVideos[0].file_url}
controls
autoPlay
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain", borderRadius: 12 }}
poster={generatedVideos[0].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>
{/* 视频预览弹窗 */}
<Modal
className="xx-preview-modal"
open={previewModalOpen}
onCancel={() => setPreviewModalOpen(false)}
footer={null}
width="80vw"
centered
destroyOnClose
>
{previewVideo && (
<div className="xx-preview-modal-content">
<video
src={previewVideo.download_url || previewVideo.file_url}
controls
autoPlay
style={{ width: "100%", maxHeight: "70vh", objectFit: "contain" }}
poster={previewVideo.thumbnail_url || undefined}
/>
</div>
)}
</Modal>
{/* 音色克隆弹窗 */}
<CloneModal
open={cloneModalOpen}
@@ -76,12 +76,59 @@ function buildPlaybackSegments(
const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
assets,
template,
videoRatio: _videoRatio,
videoRatio,
ready,
voiceAudioUrl,
titleSettings,
}) => {
const segments = useMemo(() => buildPlaybackSegments(assets, template), [assets, template])
// ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ──
const TITLE_MARGIN_TOP = 120
const TITLE_MARGIN_BOTTOM = 60
const TITLE_MARGIN_SIDE = 40
const playRes = (() => {
switch (videoRatio) {
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 }
}
})()
const playerContainerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(0)
useEffect(() => {
const el = playerContainerRef.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()
}, [])
// 标题字号按容器高度与 PlayResY 的比例缩放
const titleFontSizePx =
containerHeight > 0
? ((titleSettings?.size ?? 36) / playRes.height) * containerHeight
: (titleSettings?.size ?? 36)
const titleSidePct = (TITLE_MARGIN_SIDE / playRes.width) * 100
const titleTopPct = (TITLE_MARGIN_TOP / playRes.height) * 100
const titleBottomPct = (TITLE_MARGIN_BOTTOM / playRes.height) * 100
// 描边/阴影也要按缩放比例放大
const titleScale = containerHeight > 0 ? containerHeight / playRes.height : 1
const titleStrokeWidth = Math.max(1, 2 * titleScale)
const titleShadowBlur = 4 * titleScale
const titleShadowOffset = 2 * titleScale
// 默认走原生 video 播放(浏览器硬件解码,独立线程,不阻塞 UI)
// WebCodecs 仅在明确需要时启用(保留代码作为兜底)
const useWebCodecs = false
@@ -374,6 +421,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
return (
<div
ref={playerContainerRef}
style={{
position: "relative",
width: "100%",
@@ -434,48 +482,55 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
/>
))}
{/* 标题CSS叠加层 — video fallback 路径也要渲染 */}
{/* 标题CSS叠加层 — 与后端 ASS 烧录坐标系 1:1 对齐 */}
{titleSettings?.title && (
<div
style={{
position: "absolute",
left: 0,
right: 0,
inset: 0,
zIndex: 5,
display: "flex",
justifyContent: "center",
pointerEvents: "none",
...(titleSettings.position === "top"
? { top: "10%" }
: titleSettings.position === "center"
? { top: "50%", transform: "translateY(-50%)" }
: { bottom: "15%" }),
overflow: "hidden",
}}
>
<span
<div
style={{
fontSize: titleSettings.size,
fontFamily: titleSettings.font || "思源黑体",
color: titleSettings.color || "#ffffff",
fontWeight: titleSettings.bold ? 700 : 400,
fontStyle: titleSettings.italic ? "italic" : "normal",
textShadow: [
titleSettings.shadow ? "0 2px 8px rgba(0,0,0,0.7)" : undefined,
titleSettings.stroke
? "1px 1px 0 rgba(0,0,0,0.5), -1px -1px 0 rgba(0,0,0,0.5), 1px -1px 0 rgba(0,0,0,0.5), -1px 1px 0 rgba(0,0,0,0.5)"
: undefined,
"0 1px 3px rgba(0,0,0,0.4)",
]
.filter(Boolean)
.join(", "),
maxWidth: "90%",
position: "absolute",
left: `${titleSidePct}%`,
right: `${titleSidePct}%`,
textAlign: "center",
lineHeight: 1.3,
wordBreak: "break-word",
...(titleSettings.position === "top"
? { top: `${titleTopPct}%` }
: titleSettings.position === "center"
? { top: "50%", transform: "translateY(-50%)" }
: { bottom: `${titleBottomPct}%` }),
}}
>
{titleSettings.title}
</span>
<span
style={{
fontSize: `${titleFontSizePx}px`,
fontFamily: titleSettings.font || "思源黑体",
color: titleSettings.color || "#ffffff",
fontWeight: titleSettings.bold ? 700 : 400,
fontStyle: titleSettings.italic ? "italic" : "normal",
lineHeight: 1.05,
wordBreak: "break-word",
WebkitTextStroke: titleSettings.stroke
? `${titleStrokeWidth}px #000000`
: undefined,
textShadow: titleSettings.shadow
? `${titleShadowOffset}px ${titleShadowOffset}px ${titleShadowBlur}px rgba(0,0,0,0.8)`
: undefined,
}}
>
{titleSettings.title.split(/[/]/).map((part, i) => (
<span key={i}>
{i > 0 && <br />}
{part}
</span>
))}
</span>
</div>
</div>
)}
@@ -65,8 +65,6 @@ export interface GenerateStepContentProps {
cloneModalOpen: boolean
onCloneModalOpenChange: (open: boolean) => void
/* 生成 */
generateCount: number
onGenerateCountChange: (n: number) => void
generating: boolean
generated: boolean
generateError: string | null
@@ -119,8 +117,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
voiceMode,
selectedClonedVoice,
clonedVoices,
generateCount,
onGenerateCountChange,
generating,
generated,
generateError,
@@ -226,8 +222,6 @@ export const GenerateStepContent: React.FC<GenerateStepContentProps> = (props) =
presetVoices={presetVoices}
clonedVoices={clonedVoices}
coverSettings={coverSettings}
generateCount={generateCount}
onGenerateCountChange={onGenerateCountChange}
generating={generating}
generated={generated}
generateError={generateError}
@@ -40,17 +40,33 @@ interface PreviewVideoPanelProps {
}
/* ── ASS 坐标系参数(与后端 ass_subtitle_builder.py 一致) ── */
const ASS_VIDEO_HEIGHT = 720
const ASS_TITLE_MARGIN_TOP = 60
const ASS_TITLE_MARGIN_BOTTOM = 60
const ASS_TITLE_MARGIN_SIDE = 40
const TITLE_MARGIN_TOP = 120
const TITLE_MARGIN_BOTTOM = 60
const TITLE_MARGIN_SIDE = 40
function getPositionStyle(position: string): React.CSSProperties {
const sidePercent = (ASS_TITLE_MARGIN_SIDE / 1280) * 100
/** 根据视频比例返回后端实际渲染分辨率(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: `${(ASS_TITLE_MARGIN_BOTTOM / ASS_VIDEO_HEIGHT) * 100}%`,
bottom: `${(TITLE_MARGIN_BOTTOM / playResY) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
@@ -66,7 +82,7 @@ function getPositionStyle(position: string): React.CSSProperties {
case "top":
default:
return {
top: `${(ASS_TITLE_MARGIN_TOP / ASS_VIDEO_HEIGHT) * 100}%`,
top: `${(TITLE_MARGIN_TOP / playResY) * 100}%`,
left: `${sidePercent}%`,
right: `${sidePercent}%`,
textAlign: "center",
@@ -74,11 +90,16 @@ function getPositionStyle(position: string): React.CSSProperties {
}
}
function buildTitleStyle(settings: TitleSettings, containerHeight: number): React.CSSProperties {
function buildTitleStyle(
settings: TitleSettings,
containerHeight: number,
playResY: number,
): React.CSSProperties {
// 字号按容器高度与 PlayResY 的比例缩放,不设上限(与后端一致)
const fontSizePx =
containerHeight > 0
? (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * containerHeight
: (Math.min(settings.size, 96) / ASS_VIDEO_HEIGHT) * 400
? (settings.size / playResY) * containerHeight
: (settings.size / playResY) * 400
const base: React.CSSProperties = {
fontFamily: getFontFamily(settings.font),
@@ -86,12 +107,10 @@ function buildTitleStyle(settings: TitleSettings, containerHeight: number): Reac
color: settings.color || "#ffffff",
fontWeight: settings.bold ? 700 : 400,
fontStyle: settings.italic ? "italic" : "normal",
lineHeight: 1.3,
lineHeight: 1.05,
wordBreak: "break-word",
pointerEvents: "none",
userSelect: "none",
paddingLeft: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`,
paddingRight: `${(ASS_TITLE_MARGIN_SIDE / 1280) * 100}%`,
}
if (settings.stroke) base.WebkitTextStroke = "1px #000000"
if (settings.shadow) base.textShadow = "2px 2px 4px rgba(0,0,0,0.8)"
@@ -99,7 +118,10 @@ function buildTitleStyle(settings: TitleSettings, containerHeight: number): Reac
}
/** CSS 标题实时预览覆盖层 */
const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSettings }) => {
const TitleOverlay: React.FC<{ titleSettings: TitleSettings; videoRatio: string }> = ({
titleSettings,
videoRatio,
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(400)
@@ -118,12 +140,15 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
return () => ro.disconnect()
}, [])
const { width: playResX, height: playResY } = getResolution(videoRatio)
const positionStyle = useMemo(
() => getPositionStyle(titleSettings.position),
[titleSettings.position],
() => getPositionStyle(titleSettings.position, playResX, playResY),
// eslint-disable-next-line react-hooks/exhaustive-deps
[titleSettings.position, playResX, playResY],
)
const titleStyle = useMemo(
() => buildTitleStyle(titleSettings, containerHeight),
() => buildTitleStyle(titleSettings, containerHeight, playResY),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
containerHeight,
@@ -134,6 +159,7 @@ const TitleOverlay: React.FC<{ titleSettings: TitleSettings }> = ({ titleSetting
titleSettings.italic,
titleSettings.stroke,
titleSettings.shadow,
playResY,
],
)
@@ -266,7 +292,9 @@ export const PreviewVideoPanel: React.FC<PreviewVideoPanelProps> = ({
)}
{/* 标题样式实时预览层(仅在有视频时叠加) */}
{isReady && titleSettings && <TitleOverlay titleSettings={titleSettings} />}
{isReady && titleSettings && (
<TitleOverlay titleSettings={titleSettings} videoRatio={videoRatio} />
)}
{/* stale 遮罩:配置变更提示 */}
{isStale && (
@@ -24,8 +24,6 @@ interface Step7ConfirmGenerateProps {
presetVoices: PresetVoiceItem[]
clonedVoices: VoiceClone[]
coverSettings: CoverConfig
generateCount: number
onGenerateCountChange: (count: number) => void
generating: boolean
generated: boolean
generateError: string | null
@@ -42,9 +40,6 @@ const Step7ConfirmGenerate: React.FC<Step7ConfirmGenerateProps> = (props) => {
title,
voiceName,
coverSummary,
generateCount,
handleDecrement,
handleIncrement,
generating,
generated,
generateError,
@@ -65,10 +60,6 @@ const Step7ConfirmGenerate: React.FC<Step7ConfirmGenerateProps> = (props) => {
title={title}
voiceName={voiceName}
coverSummary={coverSummary}
generateCount={generateCount}
generating={generating}
onDecrement={handleDecrement}
onIncrement={handleIncrement}
/>
<GenerationStatus
generating={generating}
@@ -1,5 +1,4 @@
import React from "react"
import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
interface SummaryCardProps {
templateName: string
@@ -7,10 +6,6 @@ interface SummaryCardProps {
title: string
voiceName: string
coverSummary: string
generateCount: number
generating: boolean
onDecrement: () => void
onIncrement: () => void
}
const SummaryCard: React.FC<SummaryCardProps> = ({
@@ -19,10 +14,6 @@ const SummaryCard: React.FC<SummaryCardProps> = ({
title,
voiceName,
coverSummary,
generateCount,
generating,
onDecrement,
onIncrement,
}) => {
return (
<div className="xx-summary-card">
@@ -46,29 +37,6 @@ const SummaryCard: React.FC<SummaryCardProps> = ({
<span className="xx-summary-label"></span>
<span className="xx-summary-value">{coverSummary}</span>
</div>
<div className="xx-summary-row">
<span className="xx-summary-label"></span>
<span className="xx-summary-value">
<div className="xx-count-stepper">
<button
className="xx-count-stepper-btn"
disabled={generateCount <= 1 || generating}
onClick={onDecrement}
>
<MinusOutlined />
</button>
<span className="xx-count-stepper-value">{generateCount}</span>
<button
className="xx-count-stepper-btn"
disabled={generateCount >= 10 || generating}
onClick={onIncrement}
>
<PlusOutlined />
</button>
<span className="xx-count-stepper-hint"></span>
</div>
</span>
</div>
</div>
)
}
@@ -94,7 +94,7 @@ const TitleStylePanel: React.FC<TitleStylePanelProps> = ({
className="xx-slider"
type="range"
min={12}
max={48}
max={128}
value={settings.size}
onChange={(e) => onUpdateSize(Number(e.target.value))}
/>
+23 -47
View File
@@ -113,6 +113,14 @@
align-items: start;
}
.xx-generate-layout.full-width {
grid-template-columns: 1fr;
}
.xx-generate-layout.full-width .xx-generate-right-col {
display: none;
}
/* ============================================================
左侧表单区 generate-form
============================================================ */
@@ -1280,53 +1288,6 @@
}
}
/* ── 生成数量步进器 ── */
.xx-count-stepper {
display: inline-flex;
align-items: center;
gap: 6px;
}
.xx-count-stepper-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid var(--border-primary, #e2e8f0);
border-radius: 8px;
background: var(--bg-surface, #fff);
color: var(--text-secondary, #64748b);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.xx-count-stepper-btn:hover:not(:disabled) {
border-color: var(--primary-400, #818cf8);
color: var(--primary-600, #4f46e5);
background: var(--primary-50, #eef2ff);
}
.xx-count-stepper-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.xx-count-stepper-value {
min-width: 24px;
text-align: center;
font-size: 16px;
font-weight: 600;
color: var(--text-primary, #1e293b);
}
.xx-count-stepper-hint {
font-size: 12px;
color: var(--text-tertiary, #94a3b8);
margin-left: 2px;
}
/* ── 素材选择模式切换 Tab ── */
.xx-material-mode-tabs {
display: flex;
@@ -2529,6 +2490,21 @@
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;
@@ -18,7 +18,6 @@ export interface UseGenerateVideoProps {
duration: number
autoSubtitles: boolean
bgm: boolean
generateCount: number
/** 当前草稿 IDURL 参数 edit_plan_id,用于后端回写任务关联) */
sourceEditPlanId?: string | null
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 confirmGeneration 复用预览产物) */
@@ -4,7 +4,6 @@
*/
import { useState } from "react"
import { useSearchParams } from "react-router-dom"
import type { GeneratedVideo } from "@/api/template-editor"
import type { EditingTemplate } from "@/api/editing-planner"
import type { CoverConfig } from "../../types/cover"
import type { PresetVoiceItem } from "@/api/voices"
@@ -68,10 +67,6 @@ export interface GenerateFormState {
cloneModalOpen: boolean
setCloneModalOpen: (open: boolean) => void
/* 生成数量 */
generateCount: number
setGenerateCount: (n: number) => void
/* 高级设置 */
videoRatio: string
duration: number
@@ -91,12 +86,6 @@ export interface GenerateFormState {
*/
sourceEditPlanId: string | null
/* 预览弹窗 */
previewVideo: GeneratedVideo | null
setPreviewVideo: (v: GeneratedVideo | null) => void
previewModalOpen: boolean
setPreviewModalOpen: (open: boolean) => void
/** 预览任务 ID(由 useStep6Cover 创建后写入,供 useGenerateVideo 复用) */
previewTaskId: string | null
setPreviewTaskId: (id: string | null) => void
@@ -155,9 +144,6 @@ export const useGenerateFormState = (): GenerateFormState => {
/* ── 克隆声音弹窗 ── */
const [cloneModalOpen, setCloneModalOpen] = useState(false)
/* ── 生成数量 ── */
const [generateCount, setGenerateCount] = useState(1)
/* ── 高级设置(隐藏但保留) ── */
const [videoRatio] = useState("9:16")
const [duration] = useState(30)
@@ -165,10 +151,6 @@ export const useGenerateFormState = (): GenerateFormState => {
const [autoSubtitles] = useState(true)
const [bgm] = useState(true)
/* ── 预览弹窗 ── */
const [previewVideo, setPreviewVideo] = useState<GeneratedVideo | null>(null)
const [previewModalOpen, setPreviewModalOpen] = useState(false)
/* ── 预览任务 IDuseStep6Cover 创建预览时写入,useGenerateVideo 复用) ── */
// 持久化到 localStoragekey 按 editPlanId/templateId 区分,刷新页面后可恢复
const previewStorageKey = editPlanId
@@ -227,8 +209,6 @@ export const useGenerateFormState = (): GenerateFormState => {
presetVoices,
cloneModalOpen,
setCloneModalOpen,
generateCount,
setGenerateCount,
videoRatio,
duration,
style,
@@ -237,10 +217,6 @@ export const useGenerateFormState = (): GenerateFormState => {
editPlanId,
sourceEditPlanId,
planConfigStr,
previewVideo,
setPreviewVideo,
previewModalOpen,
setPreviewModalOpen,
previewTaskId,
setPreviewTaskId,
storedSourceEditPlanId,
@@ -3,11 +3,9 @@
* 组合素材库加载 + 智能匹配两个子 Hook
*/
import { useCallback, useEffect, useRef } from "react"
import type { AssetItem } from "@/api/assets"
import type { TemplateSegment } from "@/api/templates/types"
import { updateEditPlanClips } from "@/api/template-editor"
import { updateEditPlanClips, createClipsFromAssets } from "@/api/template-editor"
import { formatDuration } from "../utils/formatDuration"
import { buildClipsFromAssets } from "../utils/buildClipsFromAssets"
import { useMaterialLibrary } from "./step2-materials/useMaterialLibrary"
import { useSmartMatch } from "./step2-materials/useSmartMatch"
import { useDraftAutoSave } from "./useDraftAutoSave"
@@ -72,17 +70,19 @@ export function useStep2Materials({
scheduleSave({ asset_ids: ids }, 500)
}, [selectedTemplate, materialMode, selectedMaterials, smartSelectedIds, scheduleSave])
/* ── Step2 选择素材后同步写入 edit_plan_clips(防抖 800ms,失败静默) ── */
/* ── Step2 选择素材后同步写入 edit_plan_clips(防抖 800ms,失败静默) ──
* 调用后端 POST /clips/from-assets,由后端处理:
* - 素材不够时同一素材切多个片段
* - 随机 start_time,不重复
* - required_clips_count 保证片段数与模板 segments 一致
* 先 PUT /clips(空数组)清空旧片段,再调用 from-assets 创建新片段
*/
const clipsTimerRef = useRef<ReturnType<typeof setTimeout>>()
const clipsAbortRef = useRef<AbortController | null>(null)
const templateSegmentsRef = useRef(templateSegments)
templateSegmentsRef.current = templateSegments
const selectedTemplateRef = useRef(selectedTemplate)
selectedTemplateRef.current = selectedTemplate
const materialsRef = useRef(materials)
materialsRef.current = materials
const smartMatchedRef = useRef<AssetItem[]>(smartMatch.smartMatchedResults)
smartMatchedRef.current = smartMatch.smartMatchedResults
useEffect(() => {
const tid = selectedTemplateRef.current
@@ -97,15 +97,14 @@ export function useStep2Materials({
const controller = new AbortController()
clipsAbortRef.current = controller
const clips = buildClipsFromAssets({
selectedIds: ids,
materials: materialsRef.current.items,
smartMatchedAssets: smartMatchedRef.current,
templateSegments: templateSegmentsRef.current || [],
})
const segs = templateSegmentsRef.current || []
const requiredClipsCount = segs.length > 0 ? segs.length : undefined
try {
await updateEditPlanClips(tid, clips, controller.signal)
// 1. 清空旧片段
await updateEditPlanClips(tid, [], controller.signal)
// 2. 调用后端 from-assets 接口创建片段
await createClipsFromAssets(tid, ids, "main", requiredClipsCount)
} catch (err) {
const name = (err as { name?: string })?.name
if (name !== "CanceledError" && name !== "AbortError") {
@@ -25,8 +25,6 @@ interface UseStep7GenerateProps {
presetVoices: PresetVoiceItem[]
clonedVoices: VoiceClone[]
coverSettings: CoverConfig
generateCount: number
onGenerateCountChange: (count: number) => void
generating: boolean
generated: boolean
generateError: string | null
@@ -47,8 +45,6 @@ export function useStep7Generate({
presetVoices: _presetVoices,
clonedVoices: _clonedVoices,
coverSettings,
generateCount,
onGenerateCountChange,
generating,
generated,
generateError,
@@ -90,14 +86,6 @@ export function useStep7Generate({
return { label: "即将完成", icon: "✨" }
}
const handleDecrement = () => {
onGenerateCountChange(Math.max(1, generateCount - 1))
}
const handleIncrement = () => {
onGenerateCountChange(Math.min(10, generateCount + 1))
}
const handleScrollToPreview = () => {
const el = document.querySelector(".xx-preview-section")
el?.scrollIntoView({ behavior: "smooth", block: "start" })
@@ -109,9 +97,6 @@ export function useStep7Generate({
title,
voiceName,
coverSummary,
generateCount,
handleDecrement,
handleIncrement,
generating,
generated,
generateError,
@@ -1,69 +0,0 @@
/**
* 将选中素材 + 模板 segments 构建为 edit_plan_clips 写入数据。
*
* 逻辑必须与 FrontendPreviewPlayer.tsx 中 buildPlaybackSegments 完全一致:
* assetDuration = asset.duration || asset.metadata?.duration || 30
* tplSeg = templateSegments[i] || lastSegment
* segDuration = clamp(assetDuration, tplSeg.duration_min, tplSeg.duration_max)
* start_time = 0
* // 关键:预览播放器中 endTime = min(startTime + segDuration, assetDuration)
* // 因此 clips.duration 也必须用 min(segDuration, assetDuration) 截断,
* // 避免素材实际时长比 clamp 后的 segDuration 短时,Worker 尝试读取不存在的片段
* duration = min(segDuration, assetDuration)
*
* 预览播放器(Canvas 实时预览)直接在内存中构建 segments 播放,不读 edit_plan_clips
* 本函数产出的 clips 写入 DB 后由 Worker 渲染。两边用完全相同的时长计算,
* 保证用户在编辑过程中看到的预览与最终生成视频一致。
*/
import type { AssetItem } from "@/api/assets"
import type { TemplateSegment } from "@/api/templates/types"
import type { EditPlanClipInput } from "@/api/template-editor"
interface BuildClipsOptions {
/** 选中的素材 ID 列表(按选择顺序) */
selectedIds: string[]
/** 已加载的素材列表(用于查 duration) */
materials: AssetItem[]
/** 智能匹配返回的素材(auto 模式下可能不在 materials 列表中) */
smartMatchedAssets?: AssetItem[]
/** 模板 segments */
templateSegments?: TemplateSegment[]
}
export function buildClipsFromAssets({
selectedIds,
materials,
smartMatchedAssets = [],
templateSegments = [],
}: BuildClipsOptions): EditPlanClipInput[] {
if (!selectedIds.length) return []
// 合并两个素材来源,建立 id → asset 索引
const assetMap = new Map<string, AssetItem>()
for (const a of materials) assetMap.set(a.id, a)
for (const a of smartMatchedAssets) assetMap.set(a.id, a)
const lastSeg = templateSegments[templateSegments.length - 1]
return selectedIds.map((assetId, i) => {
const asset = assetMap.get(assetId)
const assetDuration = asset?.duration || asset?.metadata?.duration || 30
const tplSeg = templateSegments[i] || lastSeg
const segDuration = tplSeg
? Math.min(tplSeg.duration_max, Math.max(tplSeg.duration_min, assetDuration))
: Math.min(assetDuration, 10)
// 与 FrontendPreviewPlayer.buildPlaybackSegments 中
// endTime = Math.min(startTime + segDuration, assetDuration)
// 保持一致:duration 不能超过素材实际时长
const duration = Math.min(segDuration, assetDuration)
return {
asset_id: assetId,
start_time: 0,
duration,
order: i,
}
})
}
@@ -512,6 +512,9 @@ def mix_with_independent_audio(
clip_filters.append("asetpts=PTS-STARTPTS")
else:
clip_filters.append("asetpts=PTS-STARTPTS")
vol = _clip_volume(clip)
if abs(vol - 1.0) >= 1e-6:
clip_filters.append(f"volume={vol:.4f}")
# aformat 归一化:concat/amix 前统一音频参数,否则不同采样率/声道会失败
clip_filters.append(AFORMAT)
filter_parts.append(f"[{input_idx}:a]{','.join(clip_filters)}[ma{input_idx}]")
@@ -204,6 +204,76 @@ def generate_and_upload_thumbnail(
Path(tmp.name).unlink(missing_ok=True)
def _extract_frames_via_mediakit(
video_path: str,
plan_id: str,
num_frames: int,
) -> list[dict] | None:
"""使用 MediaKit 智能抽帧 API 提取封面帧。
Args:
video_path: 本地视频文件路径
plan_id: 编辑计划 ID
num_frames: 需要的帧数
Returns:
帧列表 [{"image_url": str, "timestamp": float}, ...],失败返回 None
"""
import uuid
from video_processing.oss_helpers import upload_to_oss
from packages.shared.mediakit_client import get_mediakit_client
client = get_mediakit_client()
if not client.is_available:
logger.info("[thumbnail] MediaKit 未配置,跳过智能抽帧")
return None
# 1. 上传视频到 OSS 获取 URL
try:
video_storage_key = f"temp/{plan_id}/{uuid.uuid4().hex[:8]}_{Path(video_path).name}"
video_url = upload_to_oss(video_path, video_storage_key)
if not video_url:
logger.warning("[thumbnail] 视频上传 OSS 失败,无法使用 MediaKit")
return None
logger.info("[thumbnail] 视频已上传 OSS: %s", video_url[:80])
except Exception as e:
logger.warning("[thumbnail] 视频上传 OSS 异常: %s,降级到 ffmpeg", e)
return None
# 2. 调用 MediaKit 智能抽帧
try:
frames = client.extract_frames(
video_url=video_url,
strategy="SceneChange",
max_frames=num_frames * 2, # 多取一些帧供选择
)
if not frames:
logger.warning("[thumbnail] MediaKit 抽帧返回空,降级到 ffmpeg")
return None
# 选取最均匀的 num_frames 个帧
if len(frames) > num_frames:
step = len(frames) // num_frames
frames = [frames[i * step] for i in range(num_frames)]
logger.info("[thumbnail] MediaKit 抽帧成功: %d", len(frames))
return frames
except Exception as e:
logger.warning("[thumbnail] MediaKit 抽帧异常: %s,降级到 ffmpeg", e)
return None
finally:
# 清理临时视频文件
try:
from video_processing.oss_helpers import delete_from_oss
delete_from_oss(video_storage_key)
except Exception:
pass
def extract_and_upload_cover_frames(
video_path: str,
plan_id: str,
@@ -216,6 +286,8 @@ def extract_and_upload_cover_frames(
) -> list[dict]:
"""从视频中抽取多帧作为封面候选,上传到 OSS。
优先使用 MediaKit 智能抽帧,失败时降级到 ffmpeg 直接抽帧。
Args:
video_path: 视频文件路径
plan_id: 编辑计划 ID(用于生成 storage key
@@ -229,6 +301,7 @@ def extract_and_upload_cover_frames(
Returns:
封面候选列表,每项包含 {"url": str, "position": float}
"""
import httpx
from video_processing.ffmpeg_utils import probe_duration
from video_processing.oss_helpers import upload_to_oss
@@ -238,6 +311,51 @@ def extract_and_upload_cover_frames(
duration = 0.0
candidates: list[dict] = []
# 优先尝试 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)
# 叠加标题文字(如需要)
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}/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)
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)
@@ -824,6 +824,17 @@ class UnifiedRenderService:
)
audio_layer.clips.append(vo_clip)
# replace 模式:静音原视频音轨(main + broll 图层)
if tts_config.overlap_mode == "replace":
for layer in layers:
if layer.role in ("main", "broll"):
for clip in layer.clips:
clip.config["volume"] = 0
logger.info(
"TTS replace 模式:已静音原视频音轨: plan_id=%s",
self.plan.id,
)
logger.info(
"TTS 配音已添加: plan_id=%s voice_id=%s segments=%d total_%.2fs",
self.plan.id,
@@ -889,6 +900,16 @@ class UnifiedRenderService:
)
audio_layer.clips.append(vo_clip)
# 配音素材库默认替换原音:静音原视频音轨(main + broll 图层)
for layer in layers:
if layer.role in ("main", "broll"):
for clip in layer.clips:
clip.config["volume"] = 0
logger.info(
"配音素材库:已静音原视频音轨: plan_id=%s",
self.plan.id,
)
logger.info(
"配音素材库音频已添加到 audio 图层: plan_id=%s duration=%.2fs",
self.plan.id,
+12 -2
View File
@@ -12,14 +12,22 @@ RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debia
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 预装系统依赖(gcc 编译 psycopg/pg 扩展,libpq-dev 编译期,libpq5 运行期,ffmpeg 封面取帧)
# 字体修复:fonts-noto-cjk 包的 .ttc 文件混入了 Mono 变体,导致 Bold 匹配到等宽字体
# 解决方案:删除有问题的 .ttc,使用仓库内预下载的 Noto Sans SC Variable Font(不含 Mono
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
libpq5 \
ffmpeg \
fonts-noto-cjk \
fontconfig \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* \
# 删除有问题的 .ttc 文件(包含 Mono 变体)
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc
# 复制预下载的 Noto Sans SC Variable Font(包含所有字重,不含 Mono 变体)
COPY infra/fonts/NotoSansSC-VF.ttf /usr/share/fonts/opentype/noto/NotoSansSC-VF.ttf
RUN fc-cache -fv
# 创建虚拟环境
RUN python -m venv /opt/venv
@@ -43,3 +51,5 @@ RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
RUN rm -f /tmp/requirements-base.txt /tmp/requirements.txt
ENV PYTHONPATH=/app
# Rebuild trigger: ACR push retry 20260826
+12 -2
View File
@@ -12,6 +12,8 @@ RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debia
sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list 2>/dev/null || true
# 预装系统依赖(编译工具 + 运行时 + CJK 字体用于 ASS 字幕渲染)
# 字体修复:fonts-noto-cjk 包的 .ttc 文件混入了 Mono 变体,导致 Bold 匹配到等宽字体
# 解决方案:删除有问题的 .ttc,使用仓库内预下载的 Noto Sans SC Variable Font(不含 Mono
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
g++ \
@@ -20,8 +22,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ffmpeg \
libglib2.0-0 \
fonts-noto-cjk \
&& fc-cache -fv \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/* \
# 删除有问题的 .ttc 文件(包含 Mono 变体)
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
&& rm -f /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc
# 复制预下载的 Noto Sans SC Variable Font(包含所有字重,不含 Mono 变体)
COPY infra/fonts/NotoSansSC-VF.ttf /usr/share/fonts/opentype/noto/NotoSansSC-VF.ttf
RUN fc-cache -fv
# 创建虚拟环境
RUN python -m venv /opt/venv
@@ -47,3 +55,5 @@ RUN find /opt/venv -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null; \
RUN rm -f /tmp/requirements-base.txt /tmp/requirements.txt /tmp/requirements-worker.txt
ENV PYTHONPATH=/app:/app/packages
# Rebuild trigger: ACR push retry 20260826
Binary file not shown.
+85 -32
View File
@@ -20,10 +20,32 @@ logger = logging.getLogger(__name__)
# ── 常量 ──────────────────────────────────────────────────────────────────────
# Title/Subtitle 默认边距(像素)
TITLE_MARGIN_TOP = 60
TITLE_MARGIN_TOP = 120
TITLE_MARGIN_BOTTOM = 60
TITLE_MARGIN_SIDE = 40
# 字体名称映射:前端中文字体名 → 服务器实际注册名(ffmpeg/ASS 通过注册名匹配字体)
FONT_NAME_MAP: dict[str, str] = {
"思源黑体": "Noto Sans SC",
"思源宋体": "Noto Serif CJK SC",
"苹方": "Noto Sans SC",
"PingFang": "Noto Sans SC",
"微软雅黑": "Noto Sans SC",
"楷体": "Noto Serif CJK SC",
"华康俪金黑": "Noto Sans SC",
}
# ASS Fontsize 是字体 em-square 高度(含 Latin 升降部留白),
# 中文字符实际只占声明字号的约 65%~75%;浏览器 CSS font-size 让中文字符占满声明高度。
# 为让成片中文字高与前端 CSS 预览一致,写入 ASS 时对字号乘以补偿系数。
# font_size=89 → ASS Fontsize=round(89*1.35)=120,实际中文字高约 78~85px。
ASS_FONTSIZE_COMPENSATION = 1.35
def _compensate_ass_fontsize(font_size: int) -> int:
"""将 CSS 语义字号换算为 ASS Fontsize,补偿中文字符在 em-square 中的留白。"""
return max(1, round(font_size * ASS_FONTSIZE_COMPENSATION))
# ── 颜色转换 ──────────────────────────────────────────────────────────────────
@@ -117,14 +139,20 @@ def build_ass_style(
bold_val = -1 if bold else 0
italic_val = -1 if italic else 0
# 字体名称映射:前端中文字体名 → 服务器注册名,未命中则原样使用
actual_font = FONT_NAME_MAP.get(font_name, font_name)
# BackColour 用于阴影(BorderStyle=1 时 outline + shadow
back_color = primary_color
# Shadow 深度:shadow_offset[1] 作为纵向偏移
shadow_depth = shadow_offset[1] if shadow_blur > 0 else 0
# 写入 ASS Style 时对字号做补偿,使成片中文字高与前端 CSS 预览一致
ass_font_size = _compensate_ass_fontsize(font_size)
return (
f"Style: {style_name},{font_name},{font_size},{primary_color},"
f"Style: {style_name},{actual_font},{ass_font_size},{primary_color},"
f"&H000000FF,{outline_color},{back_color},"
f"{bold_val},{italic_val},0,0,100,100,0,0,"
f"1,{outline_width},{shadow_depth},{alignment},"
@@ -147,6 +175,8 @@ def escape_ass_text(text: str) -> str:
Returns:
转义后的 ASS 文本
"""
# 用户手动换行符(半角/全角斜杠)转为 ASS 硬换行(在自动换行之前优先处理)
text = text.replace("", "\\N").replace("/", "\\N")
# 将实际换行转为 ASS 硬换行
text = text.replace("\r\n", "\\N").replace("\n", "\\N").replace("\r", "\\N")
# 转义大括号(ASS 用它做样式覆盖标签)
@@ -175,7 +205,6 @@ def format_ass_time(seconds: float) -> str:
# ── 完整 ASS 内容生成 ─────────────────────────────────────────────────────────
def _wrap_title_text(
text: str,
video_width: int,
@@ -195,26 +224,36 @@ def _wrap_title_text(
if available_width <= 0:
return text
lines: list[str] = []
current_line = ""
current_width = 0.0
# 换行宽度必须与实际渲染(补偿后的 ASS Fontsize)一致,否则换行位置会错位
ass_font_size = _compensate_ass_fontsize(font_size)
for ch in text:
# CJK 字符按全角估算,其他按半角
char_width = float(font_size) if ord(ch) > 0x2E80 else font_size * 0.55
# 先按已有 \N 分段,每段独立自动换行,最后用 \N 拼回
segments = text.split("\\N")
wrapped_segments: list[str] = []
if current_width + char_width > available_width and current_line:
for seg in segments:
lines: list[str] = []
current_line = ""
current_width = 0.0
for ch in seg:
# CJK 字符按全角估算,其他按半角
char_width = float(ass_font_size) if ord(ch) > 0x2E80 else ass_font_size * 0.55
if current_width + char_width > available_width and current_line:
lines.append(current_line)
current_line = ch
current_width = char_width
else:
current_line += ch
current_width += char_width
if current_line:
lines.append(current_line)
current_line = ch
current_width = char_width
else:
current_line += ch
current_width += char_width
if current_line:
lines.append(current_line)
wrapped_segments.append("\\N".join(lines))
return "\\N".join(lines)
return "\\N".join(wrapped_segments)
def build_ass_content(
@@ -247,26 +286,40 @@ def build_ass_content(
title_config = title_config or {}
subtitle_config = subtitle_config or {}
# ── 字段名归一化:前端传 font_size/font_color,内部用 size/color ──
if "font_size" in title_config and "size" not in title_config:
title_config["size"] = title_config["font_size"]
if "font_color" in title_config and "color" not in title_config:
title_config["color"] = title_config["font_color"]
# ── 兼容前端简化格式:stroke/shadow 为 boolean 时,转换为标准 dict ──
# 前端 TitleSettings 发送 stroke=true/false, shadow=true/false
# 后端 build_ass_style 期望 stroke={enabled, color, width}, shadow={enabled, blur, offset_x, offset_y}
if title_config:
_stroke_val = title_config.get("stroke")
if isinstance(_stroke_val, bool):
title_config["stroke"] = {
"enabled": _stroke_val,
"color": "#000000",
"width": 2,
} if _stroke_val else {"enabled": False}
title_config["stroke"] = (
{
"enabled": _stroke_val,
"color": "#000000",
"width": 2,
}
if _stroke_val
else {"enabled": False}
)
_shadow_val = title_config.get("shadow")
if isinstance(_shadow_val, bool):
title_config["shadow"] = {
"enabled": _shadow_val,
"color": "#000000",
"blur": 4,
"offset_x": 2,
"offset_y": 2,
} if _shadow_val else {"enabled": False}
title_config["shadow"] = (
{
"enabled": _shadow_val,
"color": "#000000",
"blur": 4,
"offset_x": 2,
"offset_y": 2,
}
if _shadow_val
else {"enabled": False}
)
title_enabled = title_config.get("enabled", True) and bool(title_text.strip())
subtitle_enabled = subtitle_config.get("enabled", True) and bool(subtitle_text.strip())
@@ -296,7 +349,7 @@ def build_ass_content(
build_ass_style(
"TitleStyle",
font_name=title_config.get("font", "思源黑体"),
font_size=min(int(title_config.get("size", 36)), 36),
font_size=int(title_config.get("size", 36)),
primary_color=title_color,
outline_color=stroke_color,
outline_width=stroke_width,
@@ -313,7 +366,7 @@ def build_ass_content(
# 根据视频宽度和字号自动换行标题,防止超出画面
# 先 escape 特殊字符,再插入换行符 \N,避免顺序颠倒导致 \N 被转义
title_font_size = min(int(title_config.get("size", 36)), 36)
title_font_size = int(title_config.get("size", 36))
safe_title_text_raw = escape_ass_text(title_text)
safe_title_text = _wrap_title_text(safe_title_text_raw, video_width, title_font_size)
+85 -9
View File
@@ -77,12 +77,18 @@ def _distribute_one_take(
asset_durations: dict[str, float] | None = None,
) -> None:
"""ONE_TAKE: 素材按顺序依次分配给 main 类型 clips."""
used_segments: dict[str, list[tuple[float, float]]] = {}
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
for i, clip in enumerate(main_clips):
if i < len(asset_ids):
asset_id = asset_ids[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + clip.duration))
def _distribute_pip(
@@ -91,12 +97,18 @@ def _distribute_pip(
asset_durations: dict[str, float] | None = None,
) -> None:
"""PIP: 第1个素材→main(全屏背景),其余→overlay clips."""
used_segments: dict[str, list[tuple[float, float]]] = {}
# 第1个素材 → main clip
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
if main_clips and asset_ids:
asset_id = asset_ids[0]
start_time = _calc_random_start_time(asset_id, main_clips[0].duration, asset_durations)
start_time = _calc_random_start_time(asset_id, main_clips[0].duration, asset_durations, used_segments)
main_clips[0].assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + main_clips[0].duration))
# 其余素材 → overlay clips
overlay_clips = [c for c in clips if c.clip_type == "overlay"]
@@ -104,8 +116,13 @@ def _distribute_pip(
for i, clip in enumerate(overlay_clips):
if i < len(remaining):
asset_id = remaining[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + clip.duration))
def _distribute_voice_over(
@@ -114,12 +131,18 @@ def _distribute_voice_over(
asset_durations: dict[str, float] | None = None,
) -> None:
"""VOICE_OVER: 素材→main clips (B-roll)."""
used_segments: dict[str, list[tuple[float, float]]] = {}
main_clips = [c for c in clips if c.clip_type == ClipType.MAIN.value]
for i, clip in enumerate(main_clips):
if i < len(asset_ids):
asset_id = asset_ids[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + clip.duration))
def _distribute_voice_pip(
@@ -128,6 +151,7 @@ def _distribute_voice_pip(
asset_durations: dict[str, float] | None = None,
) -> None:
"""VOICE_PIP: 第1个→background, 第2个→corner_voice, 其余→b_roll."""
used_segments: dict[str, list[tuple[float, float]]] = {}
bg_clips = [c for c in clips if c.clip_type == "background"]
voice_clips = [c for c in clips if c.clip_type == "corner_voice"]
broll_clips = [c for c in clips if c.clip_type == "b_roll"]
@@ -137,15 +161,25 @@ def _distribute_voice_pip(
# 第1个 → background
if idx < len(asset_ids) and bg_clips:
asset_id = asset_ids[idx]
start_time = _calc_random_start_time(asset_id, bg_clips[0].duration, asset_durations)
start_time = _calc_random_start_time(asset_id, bg_clips[0].duration, asset_durations, used_segments)
bg_clips[0].assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + bg_clips[0].duration))
idx += 1
# 第2个 → corner_voice
if idx < len(asset_ids) and voice_clips:
asset_id = asset_ids[idx]
start_time = _calc_random_start_time(asset_id, voice_clips[0].duration, asset_durations)
start_time = _calc_random_start_time(asset_id, voice_clips[0].duration, asset_durations, used_segments)
voice_clips[0].assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + voice_clips[0].duration))
idx += 1
# 剩余 → b_roll clips
@@ -153,8 +187,13 @@ def _distribute_voice_pip(
for i, clip in enumerate(broll_clips):
if i < len(remaining):
asset_id = remaining[i]
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations)
start_time = _calc_random_start_time(asset_id, clip.duration, asset_durations, used_segments)
clip.assign_asset(asset_id, start_time=start_time)
# Record used segment
if start_time is not None and asset_durations is not None:
if asset_id not in used_segments:
used_segments[asset_id] = []
used_segments[asset_id].append((start_time, start_time + clip.duration))
# ── 随机 start_time 计算 ────────────────────────────────────────────────────
@@ -164,16 +203,19 @@ def _calc_random_start_time(
asset_id: str,
clip_duration: float,
asset_durations: dict[str, float] | None,
used_segments: dict[str, list[tuple[float, float]]] | None = None,
) -> float | None:
"""计算随机 start_time.
"""计算随机 start_time,避开已使用的时间段.
在素材总时长范围内随机取点,确保 clip_duration 不超出素材边界。
如果 asset_durations 为 None 或素材不在其中,返回 None(使用默认 0.0)。
如果提供了 used_segments,会避开已使用的时间区间。
Args:
asset_id: 素材 ID
clip_duration: 片段时长(秒)
asset_durations: 素材 ID -> 时长映射
used_segments: {asset_id: [(start1, end1), (start2, end2), ...]} 已使用的时间段
Returns:
随机 start_time 或 None
@@ -190,7 +232,41 @@ def _calc_random_start_time(
if max_start <= 0:
return 0.0
return random.uniform(0.0, max_start)
# 如果没有已使用段,直接随机
if not used_segments or asset_id not in used_segments:
return random.uniform(0.0, max_start)
# 尝试找到一个不与已使用段重叠的起始点
used = sorted(used_segments[asset_id])
max_attempts = 100
for _ in range(max_attempts):
candidate = random.uniform(0.0, max_start)
candidate_end = candidate + clip_duration
# 检查是否与任何已使用段重叠
overlap = False
for seg_start, seg_end in used:
# 两个区间 [a, b] 和 [c, d] 重叠的条件: a < d and c < b
if candidate < seg_end and seg_start < candidate_end:
overlap = True
break
if not overlap:
return candidate
# 如果尝试多次仍找不到,缩短时长使用素材末尾
# 找到最后一个已使用段之后的可用空间
last_used_end = 0.0
for _seg_start, seg_end in used:
last_used_end = max(last_used_end, seg_end)
if last_used_end < total_duration:
# 返回从最后使用点开始的位置
return min(last_used_end, max_start)
# 实在没有空间,返回0(可能会重叠,但至少能执行)
return 0.0
# ── clip_type 映射 ────────────────────────────────────────────────────────
+1
View File
@@ -15,6 +15,7 @@ logger = logging.getLogger(__name__)
# 按优先级查找 CJK 字体(Debian/Ubuntu fonts-noto-cjk 安装路径)
_FONT_CANDIDATES = (
"/usr/share/fonts/opentype/noto/NotoSansSC-VF.ttf",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc",
-1
View File
@@ -398,7 +398,6 @@ def cleanup_repo(repo, keep_count, dry_run, protected_tags, pr_sha=None, pr_open
# 注意:pr tag是pr-<sha>sha可能属于某个PR
# 简化策略:收集所有打开PR的commit sha,在白名单里的保留
print(" 模式: Gitea PR状态检查")
open_pr_shas = set()
# 这里做了简化:因为每个PR都查commits太慢,我们用另一种方式
# 对于PR tag,先尝试匹配PR编号(如果tag名里有编号),否则按时间
# 实际pr-<sha>没法直接知道PR编号,所以降级为按时间+打开PR的head sha白名单
+3 -3
View File
@@ -232,7 +232,7 @@ def main():
# 初始化git仓库
ensure_git_repo_for_push(api_url, repo, token, branch_name)
head_branch = branch_name
fix_mode = "auto_fix_and_push"
# fix_mode removed: all PRs auto-fix (2026-08-26)
# ====== PR事件处理 ======
elif event_name == "pull_request":
@@ -251,7 +251,7 @@ def main():
# 防循环检测:检查最新commit是否已经是格式修复commit
# 修复commit message 带 [skip ci-format-check] 标记,检测到则跳过
head_branch_tmp = pr_info.get("head", {}).get("ref", "")
_head_branch_tmp = pr_info.get("head", {}).get("ref", "") # noqa: F841
skip_marker = "[skip ci-format-check]"
try:
commits_url = f"{api_url}/repos/{repo}/pulls/{pr_number}/commits?limit=3"
@@ -268,7 +268,7 @@ def main():
# 所有PR都自动修复格式(不再区分人/Agent)
print("检测到格式问题,将自动修复并推送回分支")
fix_mode = "auto_fix_and_push"
# fix_mode removed: all PRs auto-fix (2026-08-26)
print("=== 检测到代码格式问题,尝试自动修复 ===")
print(f"PR #{pr_number}")
+54 -4
View File
@@ -105,9 +105,9 @@ class TestBuildAssStyle:
def test_contains_font_size(self):
result = build_ass_style("S1", font_size=36)
# Style行格式:Name, Fontname, Fontsize, ...
# Style行格式:Name, Fontname, Fontsize, ...36*1.35=48.6→49
parts = result.split(",")
assert parts[2] == "36"
assert parts[2] == "49"
def test_bold_true(self):
result = build_ass_style("S1", bold=True)
@@ -218,6 +218,14 @@ class TestEscapeAssText:
def test_chinese_text(self):
assert escape_ass_text("你好世界") == "你好世界"
def test_slash_converted_to_newline(self):
"""半角斜杠 / 应转为 ASS 硬换行。"""
assert escape_ass_text("标题一/标题二") == "标题一\\N标题二"
def test_fullwidth_slash_converted_to_newline(self):
"""全角斜杠 应转为 ASS 硬换行。"""
assert escape_ass_text("标题一/标题二") == "标题一\\N标题二"
def test_backslash_n_in_input(self):
# 文本里本身有 \n 字符串(不是换行符)
result = escape_ass_text("\\n")
@@ -408,11 +416,53 @@ class TestBuildAssContent:
title_text="T",
title_config={"size": 72},
)
# 在TitleStyle行里查找字体大小
# 在TitleStyle行里查找字体大小(字号上限已移除,72应原样保留)
for line in result.split("\n"):
if line.startswith("Style: TitleStyle"):
parts = line.split(",")
assert parts[2] == "36"
assert parts[2] == "97" # 72*1.35=97.2→97
break
def test_title_font_size_frontend_field_alias(self):
"""前端传 font_size 应归一化为内部 size 字段。"""
result = build_ass_content(
video_width=1920,
video_height=1080,
video_duration=5.0,
title_text="T",
title_config={"font_size": 48},
)
for line in result.split("\n"):
if line.startswith("Style: TitleStyle"):
parts = line.split(",")
assert parts[2] == "65" # 48*1.35=64.8→65
break
def test_title_font_color_frontend_field_alias(self):
"""前端传 font_color 应归一化为内部 color 字段。"""
result = build_ass_content(
video_width=1920,
video_height=1080,
video_duration=5.0,
title_text="T",
title_config={"font_color": "#FF0000"},
)
# 红色 → &H0000FF
assert "&H0000FF" in result
def test_title_size_takes_precedence_over_font_size(self):
"""同时传 size 和 font_size 时,size 优先。"""
result = build_ass_content(
video_width=1920,
video_height=1080,
video_duration=5.0,
title_text="T",
title_config={"size": 56, "font_size": 28},
)
for line in result.split("\n"):
if line.startswith("Style: TitleStyle"):
parts = line.split(",")
assert parts[2] == "76" # 56*1.35=75.6→76
break
def test_title_bold(self):
+98 -4
View File
@@ -79,12 +79,12 @@ class TestBuildAssStyle:
def test_minimal_style(self):
result = build_ass_style("TestStyle")
assert result.startswith("Style: TestStyle,")
assert "思源黑体" in result
assert ",48," in result
assert "Noto Sans SC" in result
assert ",65," in result # 48*1.35=64.8→65
def test_custom_font_size(self):
result = build_ass_style("Title", font_size=64)
assert ",64," in result
assert ",86," in result # 64*1.35=86.4→86
def test_bold_enabled(self):
result = build_ass_style("BoldStyle", bold=True)
@@ -152,6 +152,26 @@ class TestBuildAssStyle:
# Style: 行有 23 个字段(去掉 "Style: " 前缀后)
assert len(parts) == 23
def test_font_name_mapping_siyuan(self):
"""思源黑体 → Noto Sans SC"""
result = build_ass_style("Test", font_name="思源黑体")
assert "Noto Sans SC" in result
def test_font_name_mapping_apple(self):
"""苹方 → Noto Sans SC"""
result = build_ass_style("Test", font_name="苹方")
assert "Noto Sans SC" in result
def test_font_name_mapping_msyh(self):
"""微软雅黑 → Noto Sans SC"""
result = build_ass_style("Test", font_name="微软雅黑")
assert "Noto Sans SC" in result
def test_font_name_mapping_unknown_passthrough(self):
"""未映射字体原样使用"""
result = build_ass_style("Test", font_name="CustomFont")
assert "CustomFont" in result
# ── 文本转义 ──────────────────────────────────────────────────────────────────
@@ -190,6 +210,19 @@ class TestEscapeAssText:
def test_chinese_text(self):
assert escape_ass_text("你好世界") == "你好世界"
def test_slash_converted_to_newline(self):
"""半角斜杠 / 应转为 ASS 硬换行。"""
assert escape_ass_text("第一行/第二行") == "第一行\\N第二行"
def test_fullwidth_slash_converted_to_newline(self):
"""全角斜杠 应转为 ASS 硬换行。"""
assert escape_ass_text("第一行/第二行") == "第一行\\N第二行"
def test_mixed_slashes_and_newlines(self):
"""斜杠和换行符都应转为硬换行。"""
result = escape_ass_text("A/B\nCD")
assert result == "A\\NB\\NC\\ND"
# ── 时间格式化 ────────────────────────────────────────────────────────────────
@@ -440,7 +473,7 @@ class TestBuildAssContent:
class TestConstants:
def test_title_margin_top(self):
assert TITLE_MARGIN_TOP == 60
assert TITLE_MARGIN_TOP == 120
def test_title_margin_bottom(self):
assert TITLE_MARGIN_BOTTOM == 60
@@ -448,6 +481,7 @@ class TestConstants:
def test_title_margin_side(self):
assert TITLE_MARGIN_SIDE == 40
# ── 标题自动换行 ──────────────────────────────────────────────────────────────
@@ -491,6 +525,34 @@ class TestWrapTitleText:
"""字号为0时直接返回原文。"""
assert _wrap_title_text("测试", 1080, 0) == "测试"
def test_preserves_explicit_newline(self):
"""已有的 \\N 换行标记应保留,不被当普通字符算宽度。"""
text = "第一行\\N第二行"
result = _wrap_title_text(text, video_width=1080, font_size=48)
assert result == text
def test_explicit_newline_each_segment_wraps_independently(self):
"""\\N 分段后,每段各自自动换行。"""
# 480px 宽,48px 字号,可用 400px,每段约8个中文字
text = "这是第一段很长很长很长的内容\\N这是第二段也很长很长的内容"
result = _wrap_title_text(text, video_width=480, font_size=48)
# 应该有多个 \N:用户手动的 + 自动换行的
assert "\\N" in result
segments = result.split("\\N")
# 至少3行(两段都需要换行)
assert len(segments) >= 3
# 验证包含两段的文字
joined = result.replace("\\N", "")
assert "第一段" in joined
assert "第二段" in joined
def test_multiple_explicit_newlines(self):
"""多个 \\N 分段都应保留。"""
text = "A\\NB\\NC"
result = _wrap_title_text(text, video_width=1080, font_size=48)
assert result == text
assert result.count("\\N") == 2
def test_build_ass_content_integration(self):
"""集成测试:build_ass_content 中的标题应该自动换行。"""
long_title = "这是一段非常长的标题文字用于测试自动换行功能是否正常工作"
@@ -508,3 +570,35 @@ class TestWrapTitleText:
break
else:
pytest.fail("未找到 TitleStyle Dialogue 行")
class TestFontsizeCompensation:
"""ASS Fontsize 补偿系数(CSS 字号 → ASS em-square 字号)。"""
def test_default_48_compensated_to_65(self):
result = build_ass_style("S")
parts = result.split(",")
assert parts[2] == "65" # round(48*1.35)=65
def test_89_compensated_to_120(self):
"""实测对齐点:font_size=89 → ASS Fontsize=120。"""
result = build_ass_style("S", font_size=89)
parts = result.split(",")
assert parts[2] == "120"
def test_subtitle_also_compensated(self):
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=5.0,
subtitle_text="字幕",
subtitle_config={"size": 24},
)
sub_line = [line for line in content.splitlines() if line.startswith("Style: SubtitleStyle")][0]
fields = [f.strip() for f in sub_line.split(",")]
assert fields[2] == "32" # round(24*1.35)=32
def test_minimum_fontsize_at_least_one(self):
result = build_ass_style("S", font_size=0)
parts = result.split(",")
assert int(parts[2]) >= 1
@@ -0,0 +1,396 @@
"""测试编辑器 from-assets 端点:同一素材切多个片段 + 随机起始 + 去重.
覆盖:
- required_clips_count 精确控制片段数量
- 素材不足时同一素材轮询切多个片段
- 随机 start_time + used_segments 去重
- 素材时长不足时 clip duration 缩短
- 向后兼容(不传 required_clips_count 时等于素材数量)
- order 追加到时间线末尾
- start_time=None 时抛出 400
- create_clip 失败时抛出 400 并记录日志
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
import pytest
from fastapi import HTTPException
TEST_PLAN_ID = "plan-draft-001"
TEST_USER_ID = "user-001"
def _make_auth_user():
auth = MagicMock()
auth.user.id = TEST_USER_ID
auth.user.email = "test@example.com"
auth.user.display_name = "测试用户"
auth.user_id = TEST_USER_ID
return auth
def _make_mock_clip(clip_id, order, duration, start_time=0.0, asset_id=""):
clip = MagicMock()
clip.id = clip_id
clip.plan_id = TEST_PLAN_ID
clip.clip_type = "main"
clip.order = order
clip.duration = duration
clip.start_time = start_time
clip.text_content = ""
clip.transition_effect = "cut"
clip.transition_duration = 0.0
clip.playback_speed = 1.0
clip.config = {}
clip.asset_id = asset_id
clip.status = "pending"
clip.template_clip_config_id = ""
clip.created_at = None
clip.updated_at = None
return clip
def _make_mock_asset(asset_id, duration):
asset = MagicMock()
asset.id = asset_id
asset.duration = duration
return asset
def _create_clips(plan_id, clip_type, order, duration=0.0, start_time=0.0, asset_id="", **kw):
return _make_mock_clip(
clip_id=f"clip-{order}",
order=order,
duration=duration,
start_time=start_time,
asset_id=asset_id,
)
def _make_plan_svc(existing_clips=None):
svc = MagicMock()
svc.get_plan_or_raise = MagicMock()
svc.create_clip = MagicMock(side_effect=_create_clips)
svc.list_clips = MagicMock(return_value=existing_clips or [])
return svc
class TestEditorClipsRequiredCount:
"""测试 required_clips_count 控制片段数量 + 同素材多片段."""
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_creates_exactly_required_clips_count(self, mock_storage):
"""required_clips_count=4 时,即使只有2个素材也创建4个片段."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=lambda aid: _make_mock_asset(aid, {"a1": 30.0, "a2": 20.0}[aid]))
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2"], required_clips_count=4)
result = create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert result.created_count == 4
assert mock_plan_svc.create_clip.call_count == 4
# 验证轮询分配:a1, a2, a1, a2
calls = mock_plan_svc.create_clip.call_args_list
assert calls[0].kwargs["asset_id"] == "a1"
assert calls[1].kwargs["asset_id"] == "a2"
assert calls[2].kwargs["asset_id"] == "a1"
assert calls[3].kwargs["asset_id"] == "a2"
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_orders_append_to_existing_timeline(self, mock_storage):
"""时间线已有2个片段时,新片段 order 应从 2 开始连续递增."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
# 模拟已有 order=0, order=1 的片段
existing = [_make_mock_clip("old-1", 0, 5.0), _make_mock_clip("old-2", 1, 5.0)]
mock_plan_svc = _make_plan_svc(existing_clips=existing)
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
calls = mock_plan_svc.create_clip.call_args_list
assert calls[0].kwargs["order"] == 2
assert calls[1].kwargs["order"] == 3
assert calls[2].kwargs["order"] == 4
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_orders_start_at_zero_when_empty(self, mock_storage):
"""空时间线时 order 从 0 开始."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc(existing_clips=[])
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
calls = mock_plan_svc.create_clip.call_args_list
assert calls[0].kwargs["order"] == 0
assert calls[1].kwargs["order"] == 1
assert calls[2].kwargs["order"] == 2
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_backward_compatible_default_count(self, mock_storage):
"""不传 required_clips_count 时,片段数等于素材数(向后兼容)."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=lambda aid: _make_mock_asset(aid, 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2", "a3"])
result = create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert result.created_count == 3
assert mock_plan_svc.create_clip.call_count == 3
class TestEditorClipsDurationAndStartTime:
"""测试素材时长获取、clip duration 缩短、start_time 传入."""
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_clip_duration_shortened_for_short_assets(self, mock_storage):
"""素材只有 3s 时 clip duration 缩短到 3.0."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("short", 3.0))
body = ClipsFromAssetsRequest(asset_ids=["short"])
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
calls = mock_plan_svc.create_clip.call_args_list
assert len(calls) == 1
assert calls[0].kwargs["duration"] == 3.0
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_start_time_passed_to_create_clip(self, mock_storage):
"""验证 _calc_random_start_time 返回值被传入 create_clip."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=2)
with patch(
"app.api.routes.templates_editor.clips._calc_random_start_time",
side_effect=[12.5, 18.0],
) as mock_calc:
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert mock_calc.call_count == 2
calls = mock_plan_svc.create_clip.call_args_list
assert calls[0].kwargs["start_time"] == 12.5
assert calls[1].kwargs["start_time"] == 18.0
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_asset_durations_deduped(self, mock_storage):
"""asset_ids 有重复时只查询一次素材时长."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
# a1 出现 3 次,但时长只应查一次
body = ClipsFromAssetsRequest(asset_ids=["a1", "a1", "a1"])
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
# 去重后只调用 1 次
assert mock_asset_repo.get.call_count == 1
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_used_segments_passed_to_calc(self, mock_storage):
"""同一素材切多个片段时,used_segments 应被维护并传入."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"], required_clips_count=3)
captured_used_segments = []
def fake_calc(asset_id, clip_duration, asset_durations, used_segments):
captured_used_segments.append({aid: list(segs) for aid, segs in (used_segments or {}).items()})
return (len(captured_used_segments) - 1) * 5.0
with patch(
"app.api.routes.templates_editor.clips._calc_random_start_time",
side_effect=fake_calc,
):
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert captured_used_segments[0] == {}
assert captured_used_segments[1] == {"a1": [(0.0, 5.0)]}
assert captured_used_segments[2] == {"a1": [(0.0, 5.0), (5.0, 10.0)]}
class TestEditorClipsErrorHandling:
"""测试异常处理."""
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_none_start_time_raises_400(self, mock_storage):
"""_calc_random_start_time 返回 None 时应抛出 HTTPException 400."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
# asset_repo.get 返回 None → asset_durations 为空 → _calc_random_start_time 返回 None
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=None)
body = ClipsFromAssetsRequest(asset_ids=["missing-asset"])
with pytest.raises(HTTPException) as exc_info:
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert exc_info.value.status_code == 400
assert "时长信息缺失" in exc_info.value.detail
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_create_clip_value_error_raises_400(self, mock_storage):
"""create_clip 抛出 ValueError 时应转为 HTTPException 400."""
from app.api.routes.templates_editor.clips import (
create_clips_from_assets_editor,
)
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_plan_svc = _make_plan_svc()
mock_plan_svc.create_clip = MagicMock(side_effect=ValueError("计划不存在"))
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(return_value=_make_mock_asset("a1", 30.0))
body = ClipsFromAssetsRequest(asset_ids=["a1"])
with pytest.raises(HTTPException) as exc_info:
create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
plan_id=TEST_PLAN_ID,
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
current_user=_make_auth_user(),
)
assert exc_info.value.status_code == 400
assert "创建片段失败" in exc_info.value.detail
@@ -321,6 +321,27 @@ class TestThumbnailInDedupHelpers:
video_processing.dedup = mock_dedup
video_processing.thumbnail_generator = mock_thumb
@classmethod
def teardown_class(cls):
"""恢复 setup_class 中替换的模块,避免污染后续测试。"""
import sys
import video_processing
# 从 sys.modules 移除 mock 模块
for mod_name in ("video_processing.thumbnail_generator", "video_processing.dedup"):
sys.modules.pop(mod_name, None)
# 重新导入真实模块以恢复 sys.modules
try:
import video_processing.thumbnail_generator # noqa: F401
except Exception:
pass
try:
import video_processing.dedup # noqa: F401
except Exception:
pass
def test_pre_generated_thumbnail_url_is_reused(self):
"""传入 thumbnail_url 时直接复用,统一封面管道不再自动生成缩略图。"""
from video_processing.dedup_helpers import create_video_record_and_dedup
+117
View File
@@ -886,3 +886,120 @@ class TestTemplateConfigPropagation:
assert clip_cfg.get("speed_ratio") == 1.2
assert clip_cfg.get("name") == "开场"
assert clip_cfg.get("custom_field") == "value"
class TestAssetDurationsAlwaysFetched:
"""验证 asset_durations 不再受 random_preview 条件限制。
修复前:asset_durations 仅在 random_preview=True 时传入 distribute_assets
修复后:只要 _asset_repo 存在,就始终获取 asset_durations
"""
def _make_service_with_asset_repo(self):
"""创建带 mock asset_repo 的 PlanGeneratorService"""
from apps.api.app.services.plan_generator_service import PlanGeneratorService
plan_repo = StubEditPlanRepository()
clip_repo = StubEditPlanClipRepository()
# mock asset_repo: 返回带 duration 的素材
asset_repo = MagicMock()
def fake_get(asset_id):
mock_asset = MagicMock()
mock_asset.duration = 30.0 # 每个素材 30 秒
return mock_asset
asset_repo.get = MagicMock(side_effect=fake_get)
with (
patch(
"apps.api.app.services.plan_generator_service.SQLAlchemyEditPlanRepository",
return_value=plan_repo,
),
patch(
"apps.api.app.services.plan_generator_service.SQLAlchemyEditPlanClipRepository",
return_value=clip_repo,
),
):
db = MagicMock()
svc = PlanGeneratorService(db, asset_repo=asset_repo)
svc._plan_repo = plan_repo
svc._clip_repo = clip_repo
return svc, asset_repo
def test_asset_durations_fetched_without_random_preview(self):
"""random_preview=False 时也应获取 asset_durations"""
svc, asset_repo = self._make_service_with_asset_repo()
template = _make_template("one_take")
clip_configs = _make_clip_configs(
template_id=template.id,
specs=[{"clip_type": ClipType.MAIN, "order": 0, "min_duration": 3.0, "max_duration": 5.0}],
)
# patch distribute_assets 以捕获传入的参数
with patch("apps.api.app.services.plan_generator_service.distribute_assets") as mock_distribute:
svc.generate_from_template(
template=template,
clip_configs=clip_configs,
asset_ids=["a1", "a2"],
random_preview=False, # 关键:非随机预览模式
)
# 验证 asset_durations 被传入(不是 None
mock_distribute.assert_called_once()
call_kwargs = mock_distribute.call_args
asset_durations = call_kwargs.kwargs.get("asset_durations", call_kwargs[1].get("asset_durations"))
assert asset_durations is not None, "asset_durations should be fetched even when random_preview=False"
assert "a1" in asset_durations
assert "a2" in asset_durations
assert asset_durations["a1"] == 30.0
# 验证 asset_repo.get 被调用(说明 _fetch_asset_durations 执行了)
assert asset_repo.get.call_count >= 2
def test_asset_durations_fetched_with_random_preview(self):
"""random_preview=True 时仍正常获取 asset_durations(行为不变)"""
svc, asset_repo = self._make_service_with_asset_repo()
template = _make_template("one_take")
clip_configs = _make_clip_configs(
template_id=template.id,
specs=[{"clip_type": ClipType.MAIN, "order": 0, "min_duration": 3.0, "max_duration": 5.0}],
)
with patch("apps.api.app.services.plan_generator_service.distribute_assets") as mock_distribute:
svc.generate_from_template(
template=template,
clip_configs=clip_configs,
asset_ids=["a1"],
random_preview=True,
)
call_kwargs = mock_distribute.call_args
asset_durations = call_kwargs.kwargs.get("asset_durations", call_kwargs[1].get("asset_durations"))
assert asset_durations is not None
assert "a1" in asset_durations
def test_no_asset_repo_means_no_durations(self):
"""_asset_repo 为 None 时 asset_durations 应为 None"""
svc, _, _ = _make_generator() # 默认不带 asset_repo
template = _make_template("one_take")
clip_configs = _make_clip_configs(
template_id=template.id,
specs=[{"clip_type": ClipType.MAIN, "order": 0, "min_duration": 3.0, "max_duration": 5.0}],
)
with patch("apps.api.app.services.plan_generator_service.distribute_assets") as mock_distribute:
svc.generate_from_template(
template=template,
clip_configs=clip_configs,
asset_ids=["a1"],
)
call_kwargs = mock_distribute.call_args
asset_durations = call_kwargs.kwargs.get("asset_durations", call_kwargs[1].get("asset_durations"))
assert asset_durations is None
+130
View File
@@ -10,6 +10,7 @@ from packages.domain.edit_plan_clip import EditPlanClip
from packages.domain.editing_mode import EditingMode
from packages.domain.plan_generator_utils import (
DEFAULT_CLIP_DURATION,
_calc_random_start_time,
create_clips_from_configs,
distribute_assets,
generate_default_clips,
@@ -647,3 +648,132 @@ class TestConstants:
def test_default_duration_value(self):
"""默认片段时长应为 5 秒."""
assert DEFAULT_CLIP_DURATION == 5.0
# ── 随机 start_time 与去重测试 ─────────────────────────────────────────
class TestCalcRandomStartTime:
"""_calc_random_start_time 函数测试."""
def test_no_durations_returns_none(self):
"""asset_durations 为 None 时返回 None."""
result = _calc_random_start_time("a1", 5.0, None)
assert result is None
def test_asset_not_in_durations_returns_none(self):
"""素材不在 durations 映射中时返回 None."""
result = _calc_random_start_time("a_missing", 5.0, {"a1": 60.0})
assert result is None
def test_zero_duration_returns_none(self):
"""素材时长为 0 时返回 None."""
result = _calc_random_start_time("a1", 5.0, {"a1": 0.0})
assert result is None
def test_clip_longer_than_asset_returns_zero(self):
"""片段时长 >= 素材时长时返回 0.0."""
result = _calc_random_start_time("a1", 60.0, {"a1": 30.0})
assert result == 0.0
def test_returns_value_in_range(self):
"""返回值在 [0, max_start] 范围内."""
for _ in range(50):
result = _calc_random_start_time("a1", 5.0, {"a1": 30.0})
assert result is not None
assert 0.0 <= result <= 25.0
def test_avoids_used_segments(self):
"""生成的 start_time 不与已使用段重叠."""
used = {"a1": [(10.0, 15.0), (20.0, 25.0)]}
for _ in range(100):
result = _calc_random_start_time("a1", 3.0, {"a1": 30.0}, used)
assert result is not None
end = result + 3.0
# 不应与 [10,15) 重叠
assert not (result < 15.0 and 10.0 < end)
# 不应与 [20,25) 重叠
assert not (result < 25.0 and 20.0 < end)
def test_fallback_when_no_space(self):
"""所有空间都被占用时,回退到最后已使用段之后或 0."""
used = {"a1": [(0.0, 28.0)]}
result = _calc_random_start_time("a1", 5.0, {"a1": 30.0}, used)
assert result is not None
# 应该返回 last_used_end (28.0) 但受限于 max_start (25.0),取 min
assert result == 25.0
def test_no_used_segments_for_asset(self):
"""used_segments 中没有当前 asset_id 时正常随机."""
used = {"a2": [(10.0, 15.0)]}
for _ in range(20):
result = _calc_random_start_time("a1", 5.0, {"a1": 30.0}, used)
assert result is not None
assert 0.0 <= result <= 25.0
class TestDistributeAssetsOverlapAvoidance:
"""测试 distribute_assets 中同一素材被多次使用时的去重逻辑."""
def test_same_asset_reused_no_overlap(self):
"""同一素材分配给多个 clip 时,start_time 不重叠."""
# 2 个 clip 共享同一素材,素材时长足够
clips = [
EditPlanClip.create(plan_id="p1", clip_type=ClipType.MAIN.value, order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type=ClipType.MAIN.value, order=1, duration=5.0),
]
asset_ids = ["a1", "a1"] # 同一素材用两次
durations = {"a1": 60.0}
distribute_assets(clips, asset_ids, EditingMode.ONE_TAKE.value, asset_durations=durations)
# 两个 clip 都应有 start_time
assert clips[0].start_time is not None
assert clips[1].start_time is not None
# 两段不应重叠
s1, e1 = clips[0].start_time, clips[0].start_time + 5.0
s2, e2 = clips[1].start_time, clips[1].start_time + 5.0
assert not (s1 < e2 and s2 < e1), f"Segments overlap: [{s1},{e1}) and [{s2},{e2})"
def test_different_assets_independent(self):
"""不同素材各自独立随机,互不影响."""
clips = [
EditPlanClip.create(plan_id="p1", clip_type=ClipType.MAIN.value, order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type=ClipType.MAIN.value, order=1, duration=5.0),
]
asset_ids = ["a1", "a2"]
durations = {"a1": 60.0, "a2": 60.0}
distribute_assets(clips, asset_ids, EditingMode.ONE_TAKE.value, asset_durations=durations)
assert clips[0].asset_id == "a1"
assert clips[1].asset_id == "a2"
assert clips[0].start_time is not None
assert clips[1].start_time is not None
def test_no_durations_still_works(self):
"""不传 asset_durations 时仍正常分配(start_time 为 None."""
clips = [
EditPlanClip.create(plan_id="p1", clip_type=ClipType.MAIN.value, order=0, duration=5.0),
]
distribute_assets(clips, ["a1"], EditingMode.ONE_TAKE.value)
assert clips[0].asset_id == "a1"
# start_time 默认为 0.0(模型默认值)
assert clips[0].start_time == 0.0
def test_voice_pip_overlap_avoidance(self):
"""VOICE_PIP 模式下同一素材多次使用时避免重叠."""
clips = [
EditPlanClip.create(plan_id="p1", clip_type="background", order=0, duration=5.0),
EditPlanClip.create(plan_id="p1", clip_type="b_roll", order=1, duration=5.0),
]
asset_ids = ["a1", "a1"] # 同一素材用两次
durations = {"a1": 60.0}
distribute_assets(clips, asset_ids, EditingMode.VOICE_PIP.value, asset_durations=durations)
assert clips[0].start_time is not None
assert clips[1].start_time is not None
s1, e1 = clips[0].start_time, clips[0].start_time + 5.0
s2, e2 = clips[1].start_time, clips[1].start_time + 5.0
assert not (s1 < e2 and s2 < e1), f"Segments overlap: [{s1},{e1}) and [{s2},{e2})"
+91
View File
@@ -11,6 +11,7 @@ from video_processing.render_audio import (
RenderContext,
clip_effective_duration,
clip_has_audio,
mix_with_independent_audio,
)
from video_processing.unified_render_service import ResolvedClip
@@ -153,3 +154,93 @@ class TestRenderContext:
"""音频缓存初始为空."""
ctx = _make_ctx()
assert ctx._audio_cache == {}
class TestMixWithIndependentAudioVolume:
"""测试 mix_with_independent_audio 中主视频 clip 音量滤镜是否生效。"""
def _make_clip(self, volume=None, clip_id="c1", duration=5.0):
"""创建测试用 ResolvedClip。"""
config = {}
if volume is not None:
config["volume"] = volume
return ResolvedClip(
clip_id=clip_id,
asset_id=f"a_{clip_id}",
local_path=Path(f"/tmp/{clip_id}.mp4"),
clip_type="main",
order=0,
duration=duration,
actual_duration=duration,
config=config,
)
def test_main_clip_volume_zero_applied_in_filter(self):
"""volume=0 的主视频 clip 应在 filter_complex 中包含 volume=0.0000。"""
from unittest.mock import patch
ctx = _make_ctx()
main_clip = self._make_clip(volume=0, clip_id="m1")
captured_cmd = {}
def fake_run_ffmpeg(cmd):
captured_cmd["cmd"] = cmd
with patch("video_processing.render_audio.run_ffmpeg", side_effect=fake_run_ffmpeg):
mix_with_independent_audio(
ctx=ctx,
main_clips=[main_clip],
audio_clips=[],
output_path=Path("/tmp/out.m4a"),
video_duration=5.0,
)
filter_complex = captured_cmd["cmd"][captured_cmd["cmd"].index("-filter_complex") + 1]
assert "volume=0.0000" in filter_complex, f"volume filter missing in: {filter_complex}"
def test_main_clip_default_volume_no_filter(self):
"""默认 volume=1.0 时不应添加 volume 滤镜。"""
from unittest.mock import patch
ctx = _make_ctx()
main_clip = self._make_clip(clip_id="m1") # no volume set
captured_cmd = {}
def fake_run_ffmpeg(cmd):
captured_cmd["cmd"] = cmd
with patch("video_processing.render_audio.run_ffmpeg", side_effect=fake_run_ffmpeg):
mix_with_independent_audio(
ctx=ctx,
main_clips=[main_clip],
audio_clips=[],
output_path=Path("/tmp/out.m4a"),
video_duration=5.0,
)
filter_complex = captured_cmd["cmd"][captured_cmd["cmd"].index("-filter_complex") + 1]
# 默认音量不应出现 volume= 滤镜
assert "volume=" not in filter_complex, f"unexpected volume filter in: {filter_complex}"
def test_main_clip_partial_volume_applied(self):
"""volume=0.5 的主视频 clip 应包含 volume=0.5000。"""
from unittest.mock import patch
ctx = _make_ctx()
main_clip = self._make_clip(volume=0.5, clip_id="m1")
captured_cmd = {}
def fake_run_ffmpeg(cmd):
captured_cmd["cmd"] = cmd
with patch("video_processing.render_audio.run_ffmpeg", side_effect=fake_run_ffmpeg):
mix_with_independent_audio(
ctx=ctx,
main_clips=[main_clip],
audio_clips=[],
output_path=Path("/tmp/out.m4a"),
video_duration=5.0,
)
filter_complex = captured_cmd["cmd"][captured_cmd["cmd"].index("-filter_complex") + 1]
assert "volume=0.5000" in filter_complex, f"volume filter missing in: {filter_complex}"
+3 -3
View File
@@ -80,13 +80,13 @@ class TestBuildAssStyle:
def test_basic_style(self):
style = _build_ass_style("Default")
assert style.startswith("Style: Default,")
assert "思源黑体" in style
assert "48" in style # font_size
assert "Noto Sans SC" in style
assert "65" in style # font_size 48*1.35=65
def test_custom_font(self):
style = _build_ass_style("Custom", font_name="Arial", font_size=32)
assert "Arial" in style
assert ",32," in style
assert ",43," in style # font_size 32*1.35=43
def test_bold(self):
style = _build_ass_style("Bold", bold=True)
+3 -3
View File
@@ -84,8 +84,8 @@ class TestBuildAssStyle:
"""基本样式行包含关键字段."""
line = _build_ass_style("Default")
assert line.startswith("Style: Default,")
assert "思源黑体" in line
assert "48" in line # font_size
assert "Noto Sans SC" in line
assert "65" in line # font_size 48*1.35=65
def test_bold_enabled(self):
"""加粗时Bold=-1."""
@@ -112,7 +112,7 @@ class TestBuildAssStyle:
"""自定义字号."""
line = _build_ass_style("Big", font_size=72)
parts = line.split(",")
assert parts[2] == "72" # Fontsize
assert parts[2] == "97" # Fontsize 72*1.35=97
def test_custom_alignment(self):
"""自定义对齐方式."""
+135
View File
@@ -86,3 +86,138 @@ class TestFormatSeekTime:
sec_parts = parts[2].split(".")
assert len(sec_parts) == 2
assert len(sec_parts[1]) == 2 # 两位小数
# ── MediaKit 智能抽帧集成测试 ──────────────────────────────────────────
class TestExtractFramesViaMediakit:
"""_extract_frames_via_mediakit 函数测试."""
def test_mediakit_not_configured_returns_none(self, tmp_path, monkeypatch):
"""MediaKit 未配置时返回 None."""
from video_processing.thumbnail_generator import _extract_frames_via_mediakit
# Mock get_mediakit_client 返回不可用客户端
class FakeClient:
is_available = False
monkeypatch.setattr(
"packages.shared.mediakit_client.get_mediakit_client",
lambda: FakeClient(),
)
video_file = tmp_path / "test.mp4"
video_file.write_bytes(b"fake video")
result = _extract_frames_via_mediakit(str(video_file), "plan1", 3)
assert result is None
def test_mediakit_success_returns_frames(self, tmp_path, monkeypatch):
"""MediaKit 成功时返回帧列表."""
from video_processing.thumbnail_generator import _extract_frames_via_mediakit
class FakeClient:
is_available = True
def extract_frames(self, video_url, strategy, max_frames):
return [
{"image_url": "https://example.com/frame1.jpg", "timestamp": 1.5},
{"image_url": "https://example.com/frame2.jpg", "timestamp": 3.2},
]
monkeypatch.setattr(
"packages.shared.mediakit_client.get_mediakit_client",
lambda: FakeClient(),
)
# Mock upload_to_oss
monkeypatch.setattr(
"video_processing.oss_helpers.upload_to_oss",
lambda path, key: f"https://oss.example.com/{key}",
)
# Mock httpx.get for downloading frame
import httpx
class FakeResponse:
status_code = 200
content = b"fake image data"
def raise_for_status(self):
pass
monkeypatch.setattr("httpx.get", lambda url, **kw: FakeResponse())
video_file = tmp_path / "test.mp4"
video_file.write_bytes(b"fake video")
result = _extract_frames_via_mediakit(str(video_file), "plan1", 2)
assert result is not None
assert len(result) == 2
assert result[0]["timestamp"] == 1.5
assert result[1]["timestamp"] == 3.2
def test_mediakit_failure_returns_none(self, tmp_path, monkeypatch):
"""MediaKit 调用失败时返回 None."""
from video_processing.thumbnail_generator import _extract_frames_via_mediakit
class FakeClient:
is_available = True
def extract_frames(self, video_url, strategy, max_frames):
return None
monkeypatch.setattr(
"packages.shared.mediakit_client.get_mediakit_client",
lambda: FakeClient(),
)
monkeypatch.setattr(
"video_processing.oss_helpers.upload_to_oss",
lambda path, key: f"https://oss.example.com/{key}",
)
video_file = tmp_path / "test.mp4"
video_file.write_bytes(b"fake video")
result = _extract_frames_via_mediakit(str(video_file), "plan1", 3)
assert result is None
class TestExtractAndUploadCoverFramesFallback:
"""extract_and_upload_cover_frames 降级逻辑测试."""
def test_fallback_to_ffmpeg_when_mediakit_fails(self, tmp_path, monkeypatch):
"""MediaKit 失败时降级到 ffmpeg 抽帧."""
from video_processing.thumbnail_generator import extract_and_upload_cover_frames
# Mock MediaKit 返回 None(未配置或失败)
class FakeClient:
is_available = False
monkeypatch.setattr(
"packages.shared.mediakit_client.get_mediakit_client",
lambda: FakeClient(),
)
# Mock ffmpeg 抽帧
monkeypatch.setattr(
"video_processing.thumbnail_generator.extract_first_frame",
lambda video_path, output_path, **kw: output_path,
)
# Mock upload
monkeypatch.setattr(
"video_processing.oss_helpers.upload_to_oss",
lambda path, key: f"https://oss.example.com/{key}",
)
# Mock probe_duration
monkeypatch.setattr(
"video_processing.ffmpeg_utils.probe_duration",
lambda path: 60.0,
)
video_file = tmp_path / "test.mp4"
video_file.write_bytes(b"fake video")
result = extract_and_upload_cover_frames(str(video_file), "plan1", num_frames=2)
assert len(result) == 2
assert all("url" in item for item in result)
assert all("position" in item for item in result)
+78 -14
View File
@@ -1,7 +1,7 @@
"""标题渲染前后端一致性测试。
验证 build_ass_content 生成的 ASS 样式参数与前端 drawTitleOnCanvas.ts 一致
- 字号上限 36px
- 字号不再设置上限由前端/调用方控制
- 描边宽度 2px
- 阴影 blur=4, offset=2
- boolean stroke/shadow 自动转换
@@ -10,14 +10,16 @@
import sys
from pathlib import Path
import pytest
# Ensure packages is importable
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "packages"))
from domain.ass_subtitle_builder import build_ass_content, build_ass_style
class TestFontSizeCap:
"""字号上限应与前端 Math.min(settings.size, 36) 一致"""
class TestFontSize:
"""字号处理:默认值与保留逻辑,不再做上限截断"""
def test_default_font_size_is_36(self):
"""无 size 字段时,默认字号应为 36。"""
@@ -29,7 +31,7 @@ class TestFontSizeCap:
title_text="测试标题",
title_config=config,
)
assert ",36," in content, f"默认字号应为36,实际内容: {content}"
assert ",49," in content, f"默认字号36应补偿为49(36*1.35),实际内容: {content}"
def test_size_32_preserved(self):
"""size=32 应原样使用。"""
@@ -41,10 +43,10 @@ class TestFontSizeCap:
title_text="测试标题",
title_config=config,
)
assert ",32," in content
assert ",43," in content # 32*1.35=43
def test_size_60_capped_at_36(self):
"""size=60 应被 cap 到 36"""
def test_size_60_preserved(self):
"""size=60 应原样保留(字号上限已移除)"""
config = {"size": 60}
content = build_ass_content(
video_width=1080,
@@ -53,14 +55,40 @@ class TestFontSizeCap:
title_text="测试标题",
title_config=config,
)
# 解析 Style 行的 Fontsize 字段(第3个字段,索引2)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
font_size = int(fields[2])
assert font_size == 36, f"字号60应被cap到36, 实际={font_size}"
assert font_size == 81, f"字号60应补偿为81(60*1.35), 实际={font_size}"
def test_font_size_alias_normalized(self):
"""前端传 font_size 应归一化为 size。"""
config = {"font_size": 52}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
style_line = [line for line in content.splitlines() if line.startswith("Style: TitleStyle")][0]
fields = [f.strip() for f in style_line.split(",")]
assert fields[2] == "70", f"font_size=52 应补偿为70(52*1.35), 实际={fields[2]}"
def test_font_color_alias_normalized(self):
"""前端传 font_color 应归一化为 color。"""
config = {"font_color": "#00FF00"}
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="测试标题",
title_config=config,
)
# 绿色 → &H00FF00
assert "&H00FF00" in content
def test_size_24_preserved(self):
"""size=24 应原样使用(小于36,不cap"""
"""size=24 应原样使用。"""
config = {"size": 24}
content = build_ass_content(
video_width=1080,
@@ -69,7 +97,7 @@ class TestFontSizeCap:
title_text="测试标题",
title_config=config,
)
assert ",24," in content
assert ",32," in content # 24*1.35=32.4→32
class TestBooleanStrokeNormalization:
@@ -202,9 +230,9 @@ class TestFullStyleConsistency:
fields = [f.strip() for f in style_line.split(",")]
# Fontname
assert fields[1] == "思源黑体"
# Fontsize = 28 (小于36,不cap)
assert fields[2] == "28"
assert fields[1] == "Noto Sans SC"
# Fontsize = 28*1.35=37.8→38
assert fields[2] == "38"
# Bold = -1 (True)
assert fields[7] == "-1"
# Outline width = 2 (前端默认 stroke width)
@@ -213,3 +241,39 @@ class TestFullStyleConsistency:
assert int(fields[17]) == 2
# Alignment = 8 (top)
assert int(fields[18]) == 8
class TestTitleSlashNewline:
"""用户输入 / 或 应触发标题换行。"""
def test_halfwidth_slash_in_title(self):
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="第一行/第二行",
title_config={"size": 48},
)
for line in content.splitlines():
if "Dialogue" in line and "TitleStyle" in line:
assert "\\N" in line, f"斜杠应转为换行: {line}"
assert "第一行" in line
assert "第二行" in line
break
else:
pytest.fail("未找到 TitleStyle Dialogue 行")
def test_fullwidth_slash_in_title(self):
content = build_ass_content(
video_width=1080,
video_height=1920,
video_duration=10.0,
title_text="第一行/第二行",
title_config={"size": 48},
)
for line in content.splitlines():
if "Dialogue" in line and "TitleStyle" in line:
assert "\\N" in line, f"全角斜杠应转为换行: {line}"
break
else:
pytest.fail("未找到 TitleStyle Dialogue 行")