Compare commits

..

52 Commits

Author SHA1 Message Date
xiaoxia 2c5600cfc9 feat: 素材选取评分注入随机噪声,降低成片查重率 (#1620)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 35s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m49s
CI/CD Pipeline / Validate - Style (push) Successful in 6m35s
CI/CD Pipeline / Integration Tests (push) Successful in 6m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m58s
CI/CD Pipeline / Validate - Security (push) Successful in 8m18s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m53s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m33s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m35s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m6s
CI/CD Pipeline / Unit Tests (push) Successful in 14m24s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-02 01:40:34 +08:00
saas-backend-agent c66e73e5b0 fix(test): 补齐噪声/转场补偿导致的 flaky 断言
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (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 / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m17s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m23s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m44s
AI Code Review / AI Code Review (pull_request) Successful in 5m21s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m29s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m38s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 6m46s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 7m11s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 9m50s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m24s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
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 / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 12s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 4m5s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 5m13s
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 - Style (push) Successful in 4m34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m55s
CI/CD Pipeline / Integration Tests (push) Successful in 3m43s
CI/CD Pipeline / Validate - Security (push) Successful in 4m59s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m31s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m17s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m53s
CI/CD Pipeline / Unit Tests (push) Successful in 15m23s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m42s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m7s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m26s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
- test_mediakit_smart_clips.test_assets_balanced_assignment:
  评分噪声下贪心均衡断言改为 zero-noise patch 内确定性验证
  (CI 失败:assert 1 == 2,与 test_editor_clips_random_start 同类)
- test_duration_within_segment_range:断言上界未计入 PR #1614
  转场补偿(2片段每clip +0.25s),uniform 接近上限时偶发 5.2>5.0;
  按补偿公式与舍入余量修正断言范围
2026-09-02 00:33:06 +08:00
saas-backend-agent 5f9538b5ac feat(ci): ci-base 镜像自动重建 workflow,requirements/Dockerfile 变更时自动构建推送
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m50s
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 4m7s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m43s
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 / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m1s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m19s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m12s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m30s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m26s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m41s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 13m6s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 13m43s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-09-02 00:30:23 +08:00
xiaoxia 19b45cbee1 fix(deps): pin numpy==1.26.4 到 dev 依赖,与 worker 保持一致,消除 ci-base 镜像 numpy 2.x 漂移
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
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 / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m59s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m19s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m32s
CI/CD Pipeline / Integration Tests (push) Successful in 8m23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m49s
CI/CD Pipeline / Validate - Style (push) Successful in 8m54s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 9m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m33s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 6m26s
CI/CD Pipeline / Unit Tests (push) Failing after 17m19s
CI/CD Pipeline / Validate - Security (push) Successful in 18m53s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-02 00:23:15 +08:00
xiaoxia f51f1139bf fix(deps): pin httpcore==1.0.7 + h2==4.1.0 防止 ci-base 镜像依赖漂移
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
API Base Image Build / Build API Base Image (push) Failing after 3m47s
Worker Base Image Build / Build Worker Base Image (push) Failing after 4m21s
2026-09-02 00:23:13 +08:00
xiaoxia 9c03755318 fix(products): 查重率 0 时显示 0.0% 而非 - (#1619)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
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 / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m26s
CI/CD Pipeline / Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m15s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m59s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m48s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 7m23s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m50s
CI/CD Pipeline / Validate - Security (push) Successful in 14m57s
CI/CD Pipeline / Unit Tests (push) Failing after 14m57s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-01 23:17:28 +08:00
CI Bot 25a98c33b9 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 / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 13:32:22 +00:00
xiaoxia 8920bead38 fix(title): 预设卡片间距调整 gap 1px + 列宽 52px 固定 (#1617)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m47s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m18s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Validate - Style (push) Failing after 6m38s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m22s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 7m9s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m37s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m6s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m49s
CI/CD Pipeline / Unit Tests (push) Failing after 15m7s
CI/CD Pipeline / Validate - Security (push) Successful in 20m9s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
fix(title): 预设卡片间距调整 gap 1px + 列宽 52px 固定 (#1617)
2026-09-01 21:23:41 +08:00
saas-backend-agent 827d8aafe5 feat: 素材分配顺序随机打乱 + MediaKit SceneChange智能选帧
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m26s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 5m45s
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 / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m46s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m35s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker 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 3m13s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Waiting to run
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 6m0s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 7m27s
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 - Python (mypy + alembic) (pull_request) Successful in 7m47s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 6m38s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 16m35s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 29m28s
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 / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
1. 素材分配顺序随机打乱
   - segments 处理顺序通过 random.shuffle 随机化
   - clips_data 最终按原始 segment order 排序返回
   - 打乱的是分配顺序,不影响播放顺序

2. MediaKit SceneChange 智能选帧
   - mediakit_client.py 新增 detect_scene_changes 方法
   - SceneChange 优先,失败降级到 TimeInterval(5秒间隔)
   - 后台任务 _update_mediakit_recommendations_async 改用 SceneChange
   - 各片段优先从不同镜头段选取起始时间
   - analyze_videos 保留为 fallback
   - 所有降级路径:MediaKit不可用/SceneChange失败 → 保持随机start_time

3. 测试
   - 新增 tests/unit/test_scene_change_shuffle.py (12个测试用例)
   - 更新 test_editor_clips_random_start.py 适配 shuffle 逻辑
   - 全部 108 个相关测试通过
   - ruff check 通过
2026-09-01 21:16:51 +08:00
xiaoxia c00a0d9eb0 perf(deploy): 健康检查轮询间隔从 5s 降至 3s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m24s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m6s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m24s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 4m52s
CI/CD Pipeline / Integration Tests (push) Successful in 5m14s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 7m14s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m57s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m35s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m25s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m16s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m37s
CI/CD Pipeline / Unit Tests (push) Successful in 14m44s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
- 更快检测到服务就绪状态
- 减少约 10-20s 的等待时间
2026-09-01 20:49:55 +08:00
xiaoxia 6c3db74fd3 perf(deploy): Worker 优雅关闭超时从 300s 降至 120s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Check push changed paths (push) Has been cancelled
- 大多数部署时 Worker 无长任务运行,120s 足够
- 减少不必要的等待时间
2026-09-01 20:49:54 +08:00
xiaoxia ab6717eeae perf(CI): Unit Tests 增量测试+依赖缓存优化
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
- push 事件也启用增量测试选择(仅跑改动相关的测试文件)
- 持久 runner 依赖 hash 缓存(requirements 无变化时跳过 pip install)
- 3 次独立 pip install 合并为 1 次
- numpy 缓存检查(已安装则跳过)
- 预计节省 5-10 分钟
2026-09-01 20:49:54 +08:00
xiaoxia c05026c5db fix(title): 预设间距归零 + 新增16种样式 (#1616)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m21s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m5s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m4s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 8m9s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-01 20:40:35 +08:00
xiaoxia 9d5ae7a5bc feat: 素材选取路径接入 smart_match 评分,让用过降权真正生效 (#1615)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
feat: 素材选取路径接入 smart_match 评分,让用过降权真正生效 (#1615)
2026-09-01 20:37:25 +08:00
xiaoxia 4aeb1d5b66 fix(render): 修复生成视频时长短于模板要求时长 (#1614)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 2m41s
CI/CD Pipeline / Integration Tests (push) Successful in 3m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m35s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m31s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m14s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 4m10s
CI/CD Pipeline / Unit Tests (push) Successful in 17m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m4s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 6m2s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
fix(render): 修复生成视频时长短于模板要求时长 (#1614)
2026-09-01 20:00:12 +08:00
xiaoxia 57621d9346 fix(title): 预设4项修复—间距0.5px+删tooltip+描边对齐+背景淡化 (#1613)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 13s
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 - Style (push) Successful in 3m59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m40s
CI/CD Pipeline / Integration Tests (push) Successful in 4m9s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m32s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m50s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m22s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 4m39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m11s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m22s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m26s
CI/CD Pipeline / Unit Tests (push) Failing after 15m42s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 19:37:12 +08:00
xiaoxia 0a5bd4f7e6 fix(title): 预设T字双图层渲染+间距1.5px+深色改浅色 (#1612)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m31s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m35s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m50s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m6s
CI/CD Pipeline / Integration Tests (push) Successful in 5m57s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m10s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m46s
CI/CD Pipeline / Validate - Security (push) Successful in 9m31s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m36s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m14s
CI/CD Pipeline / Unit Tests (push) Failing after 14m20s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-01 18:04:34 +08:00
xiaoxia a0c14db33c feat(asset-dedup): 素材去重机制6项优化 — 调参+警告+贪心分配 (#1611)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (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 push changed paths (push) Successful in 4s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m44s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m41s
CI/CD Pipeline / Validate - Style (push) Successful in 5m55s
CI/CD Pipeline / Integration Tests (push) Successful in 6m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m45s
CI/CD Pipeline / Validate - Security (push) Successful in 10m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m36s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m13s
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 Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-01 17:40:34 +08:00
xiaoxia b6ed45fc1b style(products+generate): 成片库播放按钮居中+卡片缩小+预设卡片紧凑化+深色T描边 (#1610)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
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 - Style (push) Successful in 5m15s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m2s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m46s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m37s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m34s
CI/CD Pipeline / Integration Tests (push) Successful in 5m31s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / Validate - Security (push) Successful in 9m8s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m6s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m39s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 6m10s
CI/CD Pipeline / Unit Tests (push) Successful in 15m56s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-01 16:49:58 +08:00
xiaoxia 0299678389 fix(ci): 自动同步 docker config 到 BuildKit 容器
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m11s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m30s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 3m52s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 4m37s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m25s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m44s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m29s
CI/CD Pipeline / Validate - Security (push) Successful in 11m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m40s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m15s
CI/CD Pipeline / Unit Tests (push) Successful in 13m14s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
- 每次确保 builder 存在后自动同步宿主机 docker config
- 防止 builder 重建后 registry 认证丢失
- 解决私有镜像拉取 401 问题
2026-09-01 16:21:56 +08:00
xiaoxia 5f18f844c1 ci: 触发干净构建(确保无并发取消) (#1609)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m51s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 1m19s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Successful in 1m27s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m44s
CI/CD Pipeline / Validate - Style (push) Successful in 4m49s
CI/CD Pipeline / Integration Tests (push) Successful in 5m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m46s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 8m24s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m37s
CI/CD Pipeline / Validate - Security (push) Successful in 10m1s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m46s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m11s
CI/CD Pipeline / Unit Tests (push) Successful in 13m48s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-01 16:00:34 +08:00
xiaoxia b48daf346b Merge pull request 'fix(web): 成片库卡片查重率显示 "-" — mapVideoToProductItem 硬编码 undefined' (#1608) from fix/web-duplicate-rate-mapping into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/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 push changed paths (push) Successful in 7s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 3m32s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m5s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 1m11s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m7s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Successful in 1m18s
CI/CD Pipeline / Integration Tests (push) Successful in 5m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m56s
CI/CD Pipeline / Validate - Security (push) Successful in 8m43s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m51s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m52s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m0s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m44s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 15:46:12 +08:00
xiaoxia 65f41b1ba4 Merge pull request 'ci: 重新触发构建(前次 Build Staging Web Image 瞬态失败)' (#1607) from ci/retrigger-build into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 3m49s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m17s
CI/CD Pipeline / Integration Tests (push) Successful in 4m34s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 15:31:14 +08:00
saas-backend-agent c11d70d9c9 fix(web): 成片库卡片查重率显示 '-' — mapVideoToProductItem 硬编码 undefined
AI Code Review / AI Code Review (pull_request) Successful in 3m43s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m42s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m59s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m53s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Successful in 3m17s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m15s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m28s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 4m49s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 7m31s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m21s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
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
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 2m2s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m39s
根因: mapVideoToProductItem 中 duplicate_rate 被硬编码为 undefined,
导致无论后端 API 返回什么值,前端永远显示 '-'。

修复:
- VideoItem 接口增加 duplicate_rate?: number 字段
- mapVideoToProductItem 改为从 video 对象读取 duplicate_rate
2026-09-01 15:14:08 +08:00
xiaoxia 783e426255 ci: 重新触发构建——前次 Build Staging Web Image 瞬态失败
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging 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 / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 4m29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m0s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m8s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m13s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 5m54s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m56s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 6m0s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m28s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 10m13s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 17m27s
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 / CI Gate (pull_request) Successful in 4s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 2m53s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 4m5s
2026-09-01 15:12:34 +08:00
xiaoxia 6fbc593916 Merge pull request 'fix(title): 拖拽时用ref直接改DOM避免React重渲染导致换行' (#1605) from fix/title-drag-no-rerender into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 3m43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m51s
CI/CD Pipeline / Validate - Style (push) Successful in 5m34s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 6m16s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 1m50s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 15:00:40 +08:00
xiaoxia 262b730aca Merge pull request 'style(title): 标题预设卡片改为深色背景大T字紧凑布局' (#1606) from fix/title-preset-card-style into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend 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 / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 14:58:55 +08:00
xiaoxia f685d689d6 ci: re-trigger build
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m2s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m44s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m19s
CI/CD Pipeline / Validate - Style (pull_request) Override
CI/CD Pipeline / Validate - Security (pull_request) Override
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Override
CI/CD Pipeline / Unit Tests (pull_request) Override
CI/CD Pipeline / Integration Tests (pull_request) Override
CI/CD Pipeline / Frontend Lint (pull_request) Override
CI/CD Pipeline / Frontend Unit Tests (pull_request) Override
CI/CD Pipeline / PR Build API Image (pull_request) Override
CI/CD Pipeline / PR Build Web Image (pull_request) Override
CI/CD Pipeline / PR Build Worker Image (pull_request) Override
CI/CD Pipeline / Build Staging API Image (pull_request) Override
CI/CD Pipeline / Build Staging Web Image (pull_request) Override
CI/CD Pipeline / Build Staging Worker Image (pull_request) Override
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Override
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Override
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Override
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Override
CI/CD Pipeline / Staging E2E Tests (pull_request) Override
CI/CD Pipeline / Staging API Integration Tests (pull_request) Override
CI/CD Pipeline / Build Production API Image (pull_request) Override
CI/CD Pipeline / Build Production Web Image (pull_request) Override
CI/CD Pipeline / Build Production Worker Image (pull_request) Override
CI/CD Pipeline / Deploy Production (pull_request) Override
CI/CD Pipeline / Production Browser E2E (pull_request) Override
CI/CD Pipeline / ACR Image Cleanup (pull_request) Override
CI/CD Pipeline / Canary Release to Production (pull_request) Override
CI/CD Pipeline / CI Gate (pull_request) Override
AI Code Review / AI Code Review (pull_request) Failing after 8m59s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m27s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m29s
2026-09-01 14:52:38 +08:00
xiaoxia 3c8b794efb ci: re-trigger build
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 5m20s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m1s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 6m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 6m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 6m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 7m0s
CI/CD Pipeline / AI Code Review (pull_request) Override
AI Code Review / AI Code Review (pull_request) Override
ACR Cleanup / ACR Image Cleanup (pull_request_target) Waiting to run
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m18s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 12m9s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 4m57s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
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
2026-09-01 14:51:06 +08:00
xiaoxia 0bf1cfa7f4 style(title): 标题预设卡片改为深色背景大T字紧凑布局
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m39s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 4m51s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / 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 / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m55s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 1m33s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 2m53s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m58s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m11s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m49s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
- .xx-title-preset-card: 52x52固定尺寸,#3a3a3a深色背景,8px圆角
- hover: #4a4a4a + #666边框; active: #409eff边框
- .xx-title-preset-preview-text: font-size 32px + font-weight 700
- TITLE_PRESETS 所有 preset 的 previewStyle.fontSize 统一为 32px
- 不改网格列数/间距/点击逻辑/预设内容
2026-09-01 14:41:33 +08:00
xiaoxia 89413d6822 fix(title): 拖拽时用ref直接改DOM避免React重渲染导致换行
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 6m59s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 14m21s
PR Automation / Auto Approve on CI Green (pull_request) Failing after 14m22s
AI Code Review / AI Code Review (pull_request) Failing after 14m23s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m24s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 3m13s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 3m39s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m44s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 7m33s
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
handleTitlePointerMove 每次 mousemove 都调用 onTitlePositionChange 触发
React 频繁重渲染,wordBreak:break-word 导致标题文字换行。

修复:拖拽过程中用 titleDragRef 直接修改 DOM style.left/top,
不触发 React 渲染;只在 handleTitlePointerUp(拖拽结束)时调用
onTitlePositionChange 保存最终位置。
2026-09-01 14:32:19 +08:00
xiaoxia adf6b517fb Merge pull request 'fix(products): 移除thumb max-height限制,修复竖屏视频卡片右侧白边' (#1604) from fix/product-thumb-white-border into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (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 / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m20s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 14:30:41 +08:00
xiaoxia f761a6bf07 fix(products): 移除thumb max-height限制,修复竖屏视频卡片右侧白边
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m49s
AI Code Review / AI Code Review (pull_request) Successful in 4m54s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m56s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 5m1s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 5m36s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 5m37s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m44s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m45s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m47s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m39s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
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
ACR Cleanup / ACR Image Cleanup (pull_request_target) Failing after 16m21s
Preview Cleanup / Cleanup Preview Environment (pull_request) Failing after 16m21s
根因:.xx-product-thumb 的 max-height:320px 在竖屏视频
(aspectRatio 9/16) 时限制宽度为180px,但卡片网格列宽约300px,
导致右侧出现白边。移除 max-height 让 aspectRatio 自然撑满列宽。
2026-09-01 14:16:15 +08:00
xiaoxia 0bc53172fc feat(dedup): 自动查重率扩大到用户全局——跨项目比对 (#1602)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
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 Web Image (push) Successful in 3m21s
CI/CD Pipeline / Integration Tests (push) Successful in 3m37s
CI/CD Pipeline / Validate - Style (push) Successful in 4m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m59s
CI/CD Pipeline / Validate - Security (push) Successful in 8m28s
CI/CD Pipeline / Unit Tests (push) Successful in 13m22s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 26m57s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 27m26s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 4m48s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m34s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m20s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 4m57s
2026-09-01 13:34:50 +08:00
xiaoxia 70c34da507 Merge pull request 'fix: 恢复 package-lock.json 修复测试依赖缺失' (#1603) from fix/restore-package-lock into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 10s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 3m53s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 13:26:47 +08:00
CI Bot 1cb5991c4b style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 3m42s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 4m49s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m57s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m56s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 6m9s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 6m43s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m17s
AI Code Review / AI Code Review (pull_request) Successful in 7m43s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 8m8s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m43s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 8m50s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 9m10s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
2026-09-01 05:25:08 +00:00
xiaoxia 9ad729d917 fix(dedup): AI Review 阻塞问题修复——DB 层排除自身 + 文档修正
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m51s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 4m15s
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 - Python (mypy + alembic) (pull_request) Successful in 5m44s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 4m15s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m49s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 6m2s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 6m32s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 3m1s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
1. DB 查询层排除 current_video_id:
   原实现在 Python 循环里排除自身,但 GeneratedVideo 记录在查重前已
   写入 DB(dedup_helpers L80 create → L126 compute_duplicate_rate),
   查询结果会包含自身 → MD5 完全匹配 → duplicate_rate 恒为 100%。
   现在在 query 构建时即排除:query.filter(id != current_video_id)

2. 文档修正:limit(200) → "遍历最近 200 个"(与实际实现一致)

3. 测试 mock 更新:filter() 现在被调用两次(scope + self-exclusion),
   所有测试改用 chainable query_mock 模式。
2026-09-01 13:17:19 +08:00
CI Bot 1225e65cc3 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
2026-09-01 05:13:39 +00:00
xiaoxia dedb076c94 fix: 恢复 package-lock.json,修复 @testing-library/dom 缺失导致的测试失败
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 4m45s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m21s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 5m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 3m43s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Successful in 4m34s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 6m34s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m34s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 4m37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m50s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 8m15s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
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
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m0s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m14s
PR #1601 中 npm install 意外删除了 @testing-library/dom 等测试依赖,
导致 Frontend Unit Tests 全部失败。恢复 package-lock.json 到 PR #1601 之前的状态。
2026-09-01 13:08:51 +08:00
xiaoxia 3a9a4334df feat(dedup): 自动查重率扩大到用户全局——跨项目比对
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m54s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m52s
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 - Python (mypy + alembic) (pull_request) Successful in 4m46s
AI Code Review / AI Code Review (pull_request) Failing after 4m56s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m53s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m16s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m54s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 7m2s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 7m10s
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (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
原 compute_duplicate_rate 仅按 project_id 比较,用户在不同项目/模板
下生成的视频互不可见,导致查重率始终为 0。

修改:
- dedup.py: compute_duplicate_rate 新增可选 user_id 参数;
  有 user_id 时按用户全局查询(跨项目),否则回退 project_id。
  查询上限从 100 提升到 200。
- dedup_helpers.py: create_video_record_and_dedup 传 user_id
  给 compute_duplicate_rate。

新增测试:
- test_duplicate_rate.py: +2 用例(跨项目匹配 / 空 user_id 回退)
- test_dedup_helpers_user_id.py: 3 用例(user_id 透传 / 空值兼容 /
  duplicate_rate 写回记录)

共 107 个相关测试全过。
2026-09-01 13:06:08 +08:00
xiaoxia a3f615a2d1 fix(ui): 标题拖拽pointerEvents修复 + T字放大28px + 成片thumb白边修复 (#1601)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 8s
CI/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 push changed paths (push) Successful in 10s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 4m0s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m52s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m44s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 3m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 2m1s
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Validate - Security (push) Successful in 9m22s
CI/CD Pipeline / Unit Tests (push) Successful in 13m20s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-01 12:56:55 +08:00
xiaoxia 1413cb80fe fix(smart-match): 过滤前置+余量/高频全过滤时空结果回退保留素材 (#1590)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m19s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m21s
CI/CD Pipeline / Validate - Style (push) Successful in 7m4s
CI/CD Pipeline / Integration Tests (push) Successful in 6m16s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 56s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Successful in 45s
CI/CD Pipeline / Validate - Security (push) Successful in 12m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m42s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 12:31:30 +08:00
xiaoxia 1a2c4b89be feat(title): 标题自由位置拖拽后端支持——pos_x/pos_y 注入 \pos 定位 (#1592)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 12:31:11 +08:00
xiaoxia 24f1ca35d5 fix(smart-match): limit 算法取 segments.length 下限,保证每片段至少 1 个素材 (#1600)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 11s
CI/CD Pipeline / Build Staging API Image (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m10s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m3s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Successful in 1m34s
CI/CD Pipeline / Retag skipped Staging API Image (push) Successful in 1m39s
CI/CD Pipeline / Validate - Style (push) Successful in 5m29s
CI/CD Pipeline / Integration Tests (push) Successful in 5m43s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 2m13s
CI/CD Pipeline / Validate - Security (push) Successful in 8m22s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 12:22:30 +08:00
xiaoxia ff40a7b5f3 feat(title): 预览画面标题自由拖拽——position=custom+PlayRes坐标提交(工单#1405方案B) (#1593)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m51s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m35s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m48s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m42s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m44s
CI/CD Pipeline / Integration Tests (push) Successful in 5m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m51s
CI/CD Pipeline / Validate - Security (push) Successful in 8m2s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 9m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 5m14s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m14s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 9m1s
CI/CD Pipeline / Unit Tests (push) Successful in 16m15s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-01 12:04:22 +08:00
xiaoxia 6ea77725a5 fix(clips): P0 自建模板 from-assets 400 直接查 clip_configs 表兜底 (#1596)
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 7s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 12:03:00 +08:00
CI Bot 2e3b2d7680 fix(ci): replace psycopg2 with psycopg in validate_migration.sh
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Failing after 3s
CI/CD Pipeline / Build Staging Web Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m4s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m57s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 36s
CI/CD Pipeline / Integration Tests (push) Successful in 5m4s
CI/CD Pipeline / Validate - Style (push) Successful in 5m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m54s
CI/CD Pipeline / Validate - Security (push) Successful in 7m21s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
Alembic migration validation was failing because psycopg2 module doesn't exist.
requirements-base.txt installs psycopg[binary]==3.2.2 (psycopg3).

Changed:
- import psycopg2 → import psycopg
- psycopg2.connect() → psycopg.connect()
2026-09-01 11:34:00 +08:00
xiaoxia 33f9f5021b perf(ci): pr-auto-scan cron 10min -> 15min on develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
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 Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Failing after 2m56s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m26s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m22s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Successful in 33s
CI/CD Pipeline / Integration Tests (push) Successful in 5m6s
CI/CD Pipeline / Validate - Style (push) Successful in 6m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m29s
CI/CD Pipeline / Validate - Security (push) Successful in 8m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m47s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m55s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m36s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 12m18s
CI/CD Pipeline / Unit Tests (push) Successful in 17m38s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-09-01 10:31:03 +08:00
xiaoxia 974ca188b3 fix(ci): sync REQUIRED_CONTEXTS_APPROVE with CI optimized job names
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 10:30:23 +08:00
xiaoxia 6e4b711ba2 fix(ci): sync context list with CI optimized job names in auto_approve.sh
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 10:30:20 +08:00
xiaoxia 83cbcd6c76 fix(ci): replace psycopg2 with psycopg3 in run_validate.sh
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 10:29:52 +08:00
xiaoxia 071d17d947 fix(ci): replace psycopg2 with psycopg3 in run_integration_tests.sh
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Has been cancelled
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) 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 / Check push changed paths (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
2026-09-01 10:29:34 +08:00
55 changed files with 2477 additions and 692 deletions
+105
View File
@@ -0,0 +1,105 @@
name: CI Base Image Build
on:
push:
branches:
- develop
- main
paths:
- 'requirements-base.txt'
- 'requirements-dev.txt'
- 'infra/docker/ci.Dockerfile'
workflow_dispatch:
inputs:
reason:
description: "触发原因"
required: false
default: "手动触发 - ci-base 镜像重建"
concurrency:
group: ci-base-image-build
cancel-in-progress: false
jobs:
build-ci-base:
name: Build CI Base Image
runs-on: runtime-builder
timeout-minutes: 60
steps:
- name: Checkout code
shell: sh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
curl -sH "Authorization: token $GITHUB_TOKEN" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/raw/scripts/ci/step_checkout.sh?ref=${GITHUB_SHA}" \
| bash
- name: Docker login to Gitea Registry
shell: sh
env:
GITEA_REGISTRY_USER: xiaoxia
GITEA_REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -eu
for i in 1 2 3; do
echo "=== Docker login 尝试 $i/3 ==="
if docker login git.xiaoxiajianji.com -u "${GITEA_REGISTRY_USER}" -p "${GITEA_REGISTRY_TOKEN}"; then
echo "✅ Docker login successful"
break
fi
echo "❌ Docker login 失败(尝试 $i/3),5s 后重试..."
sleep 5
done
- name: Build and push CI base image
shell: sh
run: |
set -eu
IMAGE="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas/ci-base"
VERSION_TAG="deps-$(date +%Y%m%d-%H%M)-${GITHUB_SHA::8}"
echo "=== Building CI base image (tags: latest, ${VERSION_TAG}) ==="
docker build --progress=plain \
-f infra/docker/ci.Dockerfile \
-t "${IMAGE}:latest" \
-t "${IMAGE}:${VERSION_TAG}" \
.
echo "✅ Image built successfully"
echo "=== Pushing ${VERSION_TAG} ==="
docker push "${IMAGE}:${VERSION_TAG}"
echo "=== Pushing latest ==="
docker push "${IMAGE}:latest"
echo "✅ Pushed to Gitea Registry"
- name: Verify image
shell: sh
run: |
set -eu
IMAGE="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas/ci-base:latest"
echo "=== Verifying pinned deps in fresh image ==="
docker run --rm "${IMAGE}" /opt/xiaoxia-ci-venv/bin/python -c \
"import httpcore, h2, numpy, httpx; print('VERSIONS:', httpcore.__version__, h2.__version__, numpy.__version__, httpx.__version__)"
- name: Notify result
if: always()
continue-on-error: true
shell: sh
env:
CI_NOTIFY_WEBHOOK: ${{ secrets.CI_NOTIFY_WEBHOOK }}
run: |
set +e
if [ "${{ job.status }}" = "success" ]; then
NOTIFY_MODE=success JOB_NAME="CI Base Image Build" python3 scripts/ci_notify.py
else
NOTIFY_MODE=failure JOB_NAME="CI Base Image Build" python3 scripts/ci_notify.py
fi
- name: Cleanup
if: always()
shell: sh
run: |
IMAGE="git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas/ci-base"
docker rmi "${IMAGE}:latest" 2>/dev/null || true
echo "Cleanup done"
+1 -1
View File
@@ -3,7 +3,7 @@ name: PR Auto Scan
# 作为短作业模式的兜底,防止事件驱动遗漏
on:
schedule:
- cron: "*/10 * * * *" # 每10分钟扫描一次(脚本自带240s墙钟上限,降频减负)
- cron: "*/15 * * * *" # 每10分钟扫描一次(脚本自带240s墙钟上限,降频减负)
workflow_dispatch:
permissions:
-1
View File
@@ -263,4 +263,3 @@ pytest --cov=packages --cov-report=html
---
**License**: MIT
<!-- CI trigger: 1788229339 -->
@@ -1,60 +0,0 @@
"""Add confirm generation fields
Revision ID: 034
Revises: 033
Create Date: 2026-07-08
确认生成 API 改造:为 generation_tasks 表添加 is_preview、source_task_id、
output_width、output_height、cover_url、custom_title 字段。
"""
import sqlalchemy as sa
from alembic import op
revision = "034"
down_revision = "033"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column(
"generation_tasks",
sa.Column("is_preview", sa.Boolean(), nullable=False, server_default=sa.true()),
)
op.add_column(
"generation_tasks",
sa.Column("source_task_id", sa.String(32), nullable=False, server_default=""),
)
op.add_column(
"generation_tasks",
sa.Column("output_width", sa.Integer(), nullable=False, server_default=sa.text("1280")),
)
op.add_column(
"generation_tasks",
sa.Column("output_height", sa.Integer(), nullable=False, server_default=sa.text("720")),
)
op.add_column(
"generation_tasks",
sa.Column("cover_url", sa.String(1000), nullable=False, server_default=""),
)
op.add_column(
"generation_tasks",
sa.Column("custom_title", sa.String(500), nullable=False, server_default=""),
)
op.create_index(
op.f("ix_generation_tasks_source_task_id"),
"generation_tasks",
["source_task_id"],
)
def downgrade() -> None:
op.drop_index(op.f("ix_generation_tasks_source_task_id"), table_name="generation_tasks")
op.drop_column("generation_tasks", "custom_title")
op.drop_column("generation_tasks", "cover_url")
op.drop_column("generation_tasks", "output_height")
op.drop_column("generation_tasks", "output_width")
op.drop_column("generation_tasks", "source_task_id")
op.drop_column("generation_tasks", "is_preview")
+322 -109
View File
@@ -23,6 +23,10 @@ import re
from app.auth import AuthenticatedUser, get_current_user
from app.core.storage import get_storage_service
from app.dependencies import get_asset_repository, get_db_session
# 默认转场时长(与 worker 端保持一致)
_DEFAULT_TRANSITION_DURATION = 0.5
from app.services.asset_segment_tracker import (
REUSE_RATIO_LIMIT,
SEGMENT_EDGE_GAP,
@@ -44,6 +48,7 @@ from packages.adapters.sqlalchemy_impl.template_repository import (
SQLAlchemyTemplateRepository,
)
from packages.domain.plan_generator_utils import _calc_random_start_time
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX, score_asset
from packages.shared.mediakit_client import get_mediakit_client
from .dependencies import get_draft_plan_id, get_editor_services
@@ -582,6 +587,47 @@ def _get_mediakit_recommendations(
return {}
def _calc_plan_internal_duplicate_rate(clips_data: list[dict]) -> float:
"""估算单条成片内部重复率(%.
检查本条成片中同一素材是否有重叠的时间区间。
重叠时长 / 成片总时长 * 100 = 内部重复率。
这是一个轻量估算,不依赖视频指纹;完整查重由 worker 异步完成。
"""
if not clips_data:
return 0.0
# 按素材分组
by_asset: dict[str, list[tuple[float, float]]] = {}
total_duration = 0.0
for c in clips_data:
aid = c.get("asset_id", "")
if not aid:
continue
start = c.get("start_time", 0.0)
end = start + c.get("duration", 0.0)
by_asset.setdefault(aid, []).append((start, end))
total_duration += c.get("duration", 0.0)
if total_duration <= 0:
return 0.0
# 检查同素材内的区间重叠
overlap_duration = 0.0
for segments in by_asset.values():
if len(segments) < 2:
continue
segments_sorted = sorted(segments, key=lambda s: s[0])
last_end = segments_sorted[0][1]
for start, end in segments_sorted[1:]:
overlap = max(0.0, min(end, last_end) - start)
if overlap > 0:
overlap_duration += overlap
last_end = max(last_end, end)
return round(overlap_duration / total_duration * 100, 1)
@router.post("/clips/from-assets", response_model=ClipsFromAssetsResponse)
def create_clips_from_assets_editor(
template_id: str,
@@ -626,10 +672,14 @@ def create_clips_from_assets_editor(
# 2. 获取素材实际时长(去重查询)
unique_asset_ids = list(dict.fromkeys(asset_ids))
asset_durations: dict[str, float] = {}
asset_smart_scores: 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)
# 计算 smart_match 综合评分,用于候选排序
smart_score, _ = score_asset(asset)
asset_smart_scores[asset_id] = smart_score
# 3. 在内存中计算所有片段数据(使用随机起始时间,不调用MediaKit)
# 读取素材 metadata 中持久化的历史已用区间(跨任务/跨调用去重),
@@ -662,19 +712,52 @@ def create_clips_from_assets_editor(
return False
return reused_durations.get(aid, 0.0) / assigned > REUSE_RATIO_LIMIT
for i, (_seg_order, dur_min, dur_max) in enumerate(segments):
# 素材耗尽标志:某轮循环中所有素材均被跳过时为 True
all_assets_exhausted = False
# 计算转场重叠补偿:每个 clip 需要额外增加的时长
# 目标:渲染后视频总时长 = 模板设定的各片段时长之和
# 公式:每 clip 增加 (n_segments - 1) * td / n_segments
n_segments = len(segments)
if n_segments > 1:
transition_compensation = (n_segments - 1) * _DEFAULT_TRANSITION_DURATION / n_segments
else:
transition_compensation = 0.0
# 打乱 segments 的处理顺序(分配素材的顺序随机化),但最终 clips_data 按原始 order 排序
shuffled_indices = list(range(len(segments)))
random.shuffle(shuffled_indices)
for idx in shuffled_indices:
_seg_order, dur_min, dur_max = segments[idx]
# 在 segment 的 duration_min ~ duration_max 之间随机取值(保留一位小数)
raw_duration = random.uniform(dur_min, dur_max)
# 加上转场补偿,确保最终输出时长 = 模板设定总时长
raw_duration += transition_compensation
# 轮询分配素材:跳过时长缺失、复用占比已超 15% 阈值的素材;
# 贪心分配素材:按"已使用次数"升序排列候选素材(使用最少的优先),
# 同次数随机打散,避免"A-B-C-D"的固定组合反复出现。
# 跳过时长缺失、复用占比已超 10% 阈值的素材;
# 选中后计算起点,若该素材可用区间耗尽且复用被闸门拒绝(calc 返回 None),
# 继续轮询下一个素材
# 继续尝试下一个素材
asset_id = ""
clip_duration = 0.0
start_time: float | None = None
n_assets = len(asset_ids)
for offset in range(n_assets):
candidate = asset_ids[(i + offset) % n_assets]
# 动态按使用次数排序:优先选使用最少的素材,同次数随机打散
asset_use_counts = {aid: len(used_segments.get(aid, [])) for aid in asset_ids}
# 排序键:smart_match 评分(注入随机噪声)→ 使用次数 → 纯随机。
# 噪声让得分接近的素材排名每次浮动,避免同一批素材反复选出相同组合,
# 从素材组合层面降低成片查重率;分差 > SCORE_RANDOM_NOISE_MAX 时排名稳定,
# 质量差距显著的素材仍保持优先级。
sorted_candidates = sorted(
asset_ids,
key=lambda aid: (
-(asset_smart_scores.get(aid, 0.0) + random.uniform(0.0, SCORE_RANDOM_NOISE_MAX)),
asset_use_counts.get(aid, 0),
random.random(),
),
)
for candidate in sorted_candidates:
candidate_total = asset_durations.get(candidate, 0.0)
if candidate_total <= 0:
continue
@@ -690,7 +773,7 @@ def create_clips_from_assets_editor(
continue
# 随机起始时间(不调用 MediaKit,保证接口快速返回);100 次避不开
# 历史区间时走受控复用回调(复用片段累加 reused_durations,回调内部
# 预判复用后占比超 15% 则拒绝并返回 None
# 预判复用后占比超 10% 则拒绝并返回 None
candidate_start = _calc_random_start_time(
candidate,
candidate_duration,
@@ -712,6 +795,7 @@ def create_clips_from_assets_editor(
if not asset_id or start_time is None:
# 所有素材时长缺失、复用占比超阈值,或区间耗尽且复用被拒 → 素材可切区间不足
all_assets_exhausted = True
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="素材可切区间不足,请补充新素材",
@@ -727,7 +811,7 @@ def create_clips_from_assets_editor(
clips_data.append(
{
"order": i,
"order": _seg_order,
"asset_id": asset_id,
"start_time": start_time,
"duration": clip_duration,
@@ -735,6 +819,9 @@ def create_clips_from_assets_editor(
}
)
# 按原始 segment order 排序,确保 clips_data 的 order 字段有序(0,1,2,3...
clips_data.sort(key=lambda c: c["order"])
# 4. 事务性替换:清空旧片段 → 创建新片段 → 标记ready(单事务,失败自动回滚)
created_count = plan_svc.replace_all_clips_transactional(plan_id, clips_data)
@@ -754,19 +841,86 @@ def create_clips_from_assets_editor(
unique_asset_ids,
)
# 6. 立即返回响应
# 6. 估算成片内部重复率(本条成片中同一素材的重叠片段时长占比)
dup_rate = _calc_plan_internal_duplicate_rate(clips_data)
duplicate_warning = None
if dup_rate > 50:
duplicate_warning = f"查重率 {dup_rate:.1f}% 超过50%,建议更换素材或模板"
logger.warning(
"from-assets 成片查重率超标: plan_id=%s dup_rate=%.1f%%",
plan_id,
dup_rate,
)
# 7. 素材耗尽提示
exhaustion_warning = None
if all_assets_exhausted and created_count < len(segments):
exhaustion_warning = (
"素材可切区间不足,部分片段使用了复用素材。" "建议:1) 补充更多素材到素材库 2) 使用不同的素材组合生成"
)
# 8. 立即返回响应
return ClipsFromAssetsResponse(
created_count=created_count,
plan_id=plan_id,
clip_ids=[],
duplicate_warning=duplicate_warning,
exhaustion_warning=exhaustion_warning,
)
def _build_scene_segments(
scene_changes: list[float],
asset_duration: float,
) -> list[tuple[float, float]]:
"""根据场景切换点构建镜头段列表.
Args:
scene_changes: 场景切换点时间戳列表(已排序,首位为 0.0)
asset_duration: 素材总时长
Returns:
镜头段列表 [(start, end), ...]
"""
segments: list[tuple[float, float]] = []
for i, ts in enumerate(scene_changes):
end = scene_changes[i + 1] if i + 1 < len(scene_changes) else asset_duration
# 只保留有效长度的镜头段(至少 0.5 秒)
if end - ts >= 0.5:
segments.append((ts, end))
return segments
def _pick_start_in_scene_segment(
seg_start: float,
seg_end: float,
clip_duration: float,
) -> float | None:
"""在镜头段内随机选取一个起始时间点.
确保 start + clip_duration <= seg_end。
若镜头段长度不足以容纳片段,返回 None。
"""
available = seg_end - seg_start - clip_duration
if available < 0:
return None
max_start = seg_start + available
return random.uniform(seg_start, max_start)
def _update_mediakit_recommendations_async( # pragma: no cover
plan_id: str,
asset_ids: list[str],
) -> None:
"""后台任务:MediaKit 智能选并更新片段的起始时间.
"""后台任务:使SceneChange 智能选并更新片段的起始时间.
优先使用 SceneChange 策略检测视频镜头切换点,将每个素材按镜头段拆分,
各片段优先从不同镜头段中选取起始时间,实现「不同片段展示不同场景」的效果。
降级策略:
1. SceneChange 优先 → detect_scene_changes 内部已含 TimeInterval 降级
2. 若 detect_scene_changes 仍返回 None → 回退到旧的 analyze_videos 方式
3. 所有方式都失败 → 保持现有随机 start_time,不影响视频生成
此函数在后台异步执行,不影响接口响应时间。
失败时静默处理,不影响已创建的片段。
@@ -788,12 +942,6 @@ def _update_mediakit_recommendations_async( # pragma: no cover
asset_repo = SQLAlchemyAssetRepository(db)
plan_svc = EditPlanService(db)
# 调用 MediaKit 获取推荐时间
recommendations = _get_mediakit_recommendations(asset_ids, asset_repo)
if not recommendations:
logger.info("后台任务: MediaKit 无推荐结果,跳过更新")
return
# 查询该 plan 的所有片段(分批获取,避免硬编码 limit 截断)
batch_size = 500
all_clips = []
@@ -816,15 +964,16 @@ def _update_mediakit_recommendations_async( # pragma: no cover
unique_asset_ids = list({getattr(c, "asset_id", "") or "" for c in clips} - {""})
assets_map: dict[str, object] = {a.id: a for a in asset_repo.find_by_ids(unique_asset_ids)}
# 按 asset_id 预分组片段时间段(消除 O(N^2) 嵌套循环
clips_by_asset: dict[str, list[tuple[str, float, float]]] = defaultdict(list)
# 按 asset_id 预分组片段对象(按 order 排序,保证按模板顺序分配镜头段
clips_by_asset: dict[str, list] = defaultdict(list)
for clip in clips:
aid = getattr(clip, "asset_id", "") or ""
if aid and clip.start_time is not None:
clips_by_asset[aid].append((clip.id, clip.start_time, clip.start_time + clip.duration))
if aid:
clips_by_asset[aid].append(clip)
for aid in clips_by_asset:
clips_by_asset[aid].sort(key=lambda c: c.order)
# 读取素材全部历史已用区间(跨任务/跨 plan 持久化记录)
# MediaKit 挪点必须与随机选片一样避让历史区间,否则会把片段挪回已用过的画面
# 读取素材全部历史已用区间(跨任务/跨 plan 持久化记录)
historical_segments = get_used_segments(db, unique_asset_ids)
# 已更新的片段ID(用于排除已移动的旧时间段)
@@ -833,16 +982,22 @@ def _update_mediakit_recommendations_async( # pragma: no cover
updated_segments: dict[str, list[tuple[float, float]]] = {}
updated_count = 0
# 遍历片段,按 asset_id 匹配推荐时间
for clip in clips:
asset_id = getattr(clip, "asset_id", "") or ""
if not asset_id or asset_id not in recommendations:
# 尝试获取存储服务(用于生成视频 URL)
try:
storage = get_storage_service()
except Exception:
logger.warning("后台任务: 获取存储服务失败,跳过 SceneChange 更新")
return
# 获取 MediaKit 客户端
client = get_mediakit_client()
# 对每个素材,检测场景切换点并分配镜头段
for asset_id in unique_asset_ids:
asset_clips = clips_by_asset.get(asset_id, [])
if not asset_clips:
continue
recommended_start = recommendations[asset_id]
clip_duration = clip.duration
# 从预加载字典获取素材(O(1) 查找)
asset = assets_map.get(asset_id)
if not asset:
continue
@@ -850,89 +1005,147 @@ def _update_mediakit_recommendations_async( # pragma: no cover
if asset_total <= 0:
continue
# 推荐时间 + 片段时长不能超过素材总时长
if recommended_start + clip_duration > asset_total:
logger.info(
"后台任务: 推荐时间越界,跳过: asset_id=%s recommended=%.2f duration=%.1f total=%.1f",
asset_id,
recommended_start,
clip_duration,
asset_total,
)
continue
# 构建排除当前片段及已更新片段后的占用列表(O(M),M=同素材片段数)
other_segments: list[tuple[float, float]] = [
(cs, ce)
for cid, cs, ce in clips_by_asset.get(asset_id, [])
if cid != clip.id and cid not in updated_clip_ids
]
other_segments.extend(updated_segments.get(asset_id, []))
# 并入该素材全部历史已用区间(含其他 plan/其他任务),set 去重:
# 本 plan 片段创建时已写入历史记录
# 并入该素材全部历史已用区间(含其他 plan/其他任务)。
# set 去重前先归一化精度(round 3 位),避免浮点尾差导致逻辑相同的
# 区间(如 1.0 与 1.0000000001)被误判为不同区间
def _norm(segs):
return {(round(float(a), 3), round(float(b), 3)) for a, b in segs}
other_segments = list(_norm(other_segments) | _norm(historical_segments.get(asset_id, [])))
# 检查推荐时间是否与同 plan 片段或历史已用区间冲突(含 0.3s 边缘间隙):
# 冲突时放弃该推荐、保留原随机起点(不硬挪到已用过的画面)
if _recommended_time_conflicts(recommended_start, clip_duration, other_segments):
logger.info(
"后台任务: 推荐时间与同片/历史区间冲突,保留原起点: asset_id=%s recommended=%.2f",
asset_id,
recommended_start,
)
continue
# 逐个更新并捕获异常(单点失败不影响其他片段)
try:
old_start = clip.start_time
old_end = old_start + clip_duration
# MediaKit 移动片段起点 + 同步素材 metadata 区间记录放在同一事务:
# 删旧区间记录(按 plan_id + 旧 start 匹配,兼容无 plan_id 的旧数据)、
# 写新区间,最后统一 commit;任一步失败整体 rollback
# 保证 clip.start_time 与 metadata.used_time_ranges 不出现不一致。
plan_svc.update_clip(clip.id, start_time=recommended_start)
# 获取素材视频 URL
video_url: str | None = None
storage_key = getattr(asset, "storage_key", None) or ""
mime = getattr(asset, "mime_type", "") or ""
if storage_key and mime.startswith("video/"):
try:
if remove_used_segment(db, asset_id, old_start, old_end, plan_id=plan_id):
record_used_segments(
db,
asset_id,
recommended_start,
recommended_start + clip_duration,
plan_id,
)
except Exception as me:
logger.warning(
"后台任务: 同步素材区间记录失败,回滚本次片段更新: clip_id=%s error=%s",
clip.id,
me,
video_url = storage.get_download_url(storage_key)
except Exception as e:
logger.warning("后台任务: 获取素材URL失败: asset_id=%s error=%s", asset_id, e)
# 构建该素材的占用区间列表(排除已更新片段)
def _get_other_segments(asset_id_inner, clip_id_inner):
segs: list[tuple[float, float]] = []
for c in clips_by_asset.get(asset_id_inner, []):
cid = c.id
if cid != clip_id_inner and cid not in updated_clip_ids:
segs.append((c.start_time, c.start_time + c.duration))
segs.extend(updated_segments.get(asset_id_inner, []))
# 并入历史已用区间
def _norm(segs_in):
return {(round(float(a), 3), round(float(b), 3)) for a, b in segs_in}
return list(_norm(segs) | _norm(historical_segments.get(asset_id_inner, [])))
# 优先使用 SceneChange 策略
scene_segments: list[tuple[float, float]] = []
if client.is_available and video_url:
scene_changes = client.detect_scene_changes(video_url)
if scene_changes is not None:
scene_segments = _build_scene_segments(scene_changes, asset_total)
logger.info(
"后台任务: 素材场景检测完成: asset_id=%s scenes=%d",
asset_id,
len(scene_segments),
)
db.rollback()
continue
db.commit()
updated_count += 1
updated_clip_ids.add(clip.id)
except Exception as ue:
logger.warning("后台任务: 单个片段更新失败: clip_id=%s error=%s", clip.id, ue)
try:
db.rollback()
except Exception:
pass
# SceneChange 未获得有效结果 → 尝试 analyze_videos 作为 fallback
if not scene_segments and video_url:
fallback_recs = _get_mediakit_recommendations([asset_id], asset_repo)
if fallback_recs and asset_id in fallback_recs:
# analyze_videos 只返回单个推荐点,转为单镜头段
rec_start = fallback_recs[asset_id]
scene_segments = [(rec_start, asset_total)]
logger.info(
"后台任务: 使用 analyze_videos fallback: asset_id=%s start=%.2f",
asset_id,
rec_start,
)
if not scene_segments:
# 所有方式都失败 → 保持现有随机 start_time
logger.info(
"后台任务: SceneChange 与 analyze_videos 均无结果,保持随机起点: asset_id=%s",
asset_id,
)
continue
updated_segments.setdefault(asset_id, []).append((recommended_start, recommended_start + clip_duration))
logger.info(
"后台任务: 更新片段起始时间: clip_id=%s asset_id=%s start_time=%.2f",
clip.id,
asset_id,
recommended_start,
)
# 为每个片段分配不同的镜头段
scene_segments_pool = list(scene_segments) # 可消费的镜头段池
for clip in asset_clips:
clip_duration = clip.duration
recommended_start: float | None = None
# 从镜头段池中依次尝试,选一个不冲突的
for seg_idx, (seg_start, seg_end) in enumerate(scene_segments_pool):
candidate_start = _pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
if candidate_start is None:
continue # 镜头段太短,跳过
# 检查越界
if candidate_start + clip_duration > asset_total:
continue
# 检查与已用区间冲突
other_segs = _get_other_segments(asset_id, clip.id)
if _recommended_time_conflicts(candidate_start, clip_duration, other_segs):
continue
recommended_start = candidate_start
# 消费该镜头段(从池中移除,下一个片段用不同镜头段)
scene_segments_pool.pop(seg_idx)
break
if recommended_start is None:
# 镜头段用完或都冲突 → 尝试 _calc_random_start_time 兜底
used_segs_for_calc: dict[str, list[tuple[float, float]]] = {
asset_id: _get_other_segments(asset_id, clip.id)
}
fallback_start = _calc_random_start_time(
asset_id,
clip_duration,
{asset_id: asset_total},
used_segs_for_calc,
)
if fallback_start is None:
continue # 完全无法分配,保持原起点
recommended_start = fallback_start
# 更新片段起始时间
try:
old_start = clip.start_time
old_end = old_start + clip_duration
plan_svc.update_clip(clip.id, start_time=recommended_start)
try:
if remove_used_segment(db, asset_id, old_start, old_end, plan_id=plan_id):
record_used_segments(
db,
asset_id,
recommended_start,
recommended_start + clip_duration,
plan_id,
)
except Exception as me:
logger.warning(
"后台任务: 同步素材区间记录失败,回滚本次片段更新: clip_id=%s error=%s",
clip.id,
me,
)
db.rollback()
continue
db.commit()
updated_count += 1
updated_clip_ids.add(clip.id)
updated_segments.setdefault(asset_id, []).append(
(recommended_start, recommended_start + clip_duration)
)
logger.info(
"后台任务: 更新片段起始时间(场景选帧): clip_id=%s asset_id=%s start_time=%.2f",
clip.id,
asset_id,
recommended_start,
)
except Exception as ue:
logger.warning("后台任务: 单个片段更新失败: clip_id=%s error=%s", clip.id, ue)
try:
db.rollback()
except Exception:
pass
continue
logger.info("后台任务完成: plan_id=%s 成功更新 %d 个片段", plan_id, updated_count)
@@ -189,6 +189,8 @@ class ClipsFromAssetsResponse(BaseModel):
plan_id: str = ""
message: str = ""
clip_ids: List[str] = Field(default_factory=list, description="创建的片段ID列表")
duplicate_warning: Optional[str] = Field(default=None, description="查重率超标警告")
exhaustion_warning: Optional[str] = Field(default=None, description="素材耗尽警告")
# ── 封面配置 ────────────────────────────────────────────────────────────────
@@ -3,7 +3,7 @@
在素材 metadataassets.classification_result JSON)中持久化已使用的片段时间区间,
供 from-assets 创建片段时避开历史区间,实现跨任务/跨调用的片段去重;
素材可用区间耗尽后进入受控复用:允许有限次数(MAX_RANGE_USE_COUNT)复用最久未用
的历史区间,配合调用方的成片复用占比控制(MAX_REUSE_RATIO = 15%),把任意两条
的历史区间,配合调用方的成片复用占比控制(MAX_REUSE_RATIO = 10%),把任意两条
成片的画面重复率控制在阈值内。
metadata 中的记录字段 ``used_time_ranges``::
@@ -40,14 +40,14 @@ logger = logging.getLogger(__name__)
USED_RANGES_KEY = "used_time_ranges"
# ── 受控复用配置常量 ─────────────────────────────────────────────────────────
MAX_RANGE_USE_COUNT = 3
MAX_RANGE_USE_COUNT = 2
"""单条历史区间最多被使用次数(含首次),达到后不再参与复用。"""
REUSE_RATIO_LIMIT = 0.15
"""单条成片中,单个素材的复用片段累计时长 / 该素材在成片中的总时长上限(15%)。
REUSE_RATIO_LIMIT = 0.10
"""单条成片中,单个素材的复用片段累计时长 / 该素材在成片中的总时长上限(10%)。
超过则该素材不再分配新片段(调用方在轮询分配时跳过)。"""
SEGMENT_EDGE_GAP = 0.3
SEGMENT_EDGE_GAP = 1.5
"""冲突判定边缘间隙(秒):历史区间按 [start-gap, end+gap] 扩边后参与冲突检测,
避免两条片段首尾紧贴导致画面观感重复;记录仍存实际值。"""
@@ -397,12 +397,12 @@ def make_reuse_callback(
db: SQLAlchemy session
asset_durations: 素材 ID -> 总时长(回调需要素材总时长做边界约束)
reused_tracker: 可选的 ``{asset_id: 累计复用时长}``,回调成功返回复用区间时
会把本次片段时长累加进去,供调用方统计成片复用占比(15% 阈值)。
会把本次片段时长累加进去,供调用方统计成片复用占比(10% 阈值)。
assigned_tracker: 可选的 ``{asset_id: 已分配片段总时长}``,配合 ratio_limit
在复用前预判:若复用本片段后占比 (reused + clip_duration) /
(assigned + clip_duration) 超过 ratio_limit,则拒绝复用、返回 None
(保证成片复用占比不超阈值)。
ratio_limit: 单条成片复用时长占比上限,默认 15%
ratio_limit: 单条成片复用时长占比上限,默认 10%
Returns:
回调函数 ``(asset_id, clip_duration) -> (start, end) | None``。
@@ -13,6 +13,7 @@
from __future__ import annotations
import logging
import random
from typing import Any, List
from sqlalchemy.orm import Session
@@ -32,6 +33,7 @@ from packages.domain.plan_generator_utils import (
generate_default_clips,
map_clip_types_for_mode,
)
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX, score_asset
from packages.domain.template_clip_config import TemplateClipConfig
logger = logging.getLogger(__name__)
@@ -218,8 +220,13 @@ class PlanGeneratorService:
) -> None:
"""按 editing_mode 将素材分配到 clips(就地修改,未持久化).
委托给 plan_generator_utils.distribute_assets 纯函数。
先用 smart_match 评分对素材排序(高分优先),再委托给
plan_generator_utils.distribute_assets 纯函数完成分配。
"""
# 用 smart_match 评分排序素材:高分(质量好/时长合适/新鲜/未使用)优先
if self._asset_repo and not random_selection:
asset_ids = self._sort_assets_by_smart_score(asset_ids)
distribute_assets(
clips,
asset_ids,
@@ -228,6 +235,29 @@ class PlanGeneratorService:
asset_durations=asset_durations,
)
def _sort_assets_by_smart_score(self, asset_ids: List[str]) -> List[str]:
"""按 smart_match 综合评分降序排列素材 ID(注入随机噪声)。
评分高的素材(质量好、时长合适、新鲜、使用次数少)倾向排在前面;
排序时给每个素材的得分注入 0~SCORE_RANDOM_NOISE_MAX 的随机噪声,
使得分接近的素材排名每次浮动,避免一键生成反复选出相同素材组合,
从素材组合层面降低成片查重率。分差大于噪声上限时排名保持稳定。
"""
scored: list[tuple[str, float]] = []
for asset_id in asset_ids:
asset = self._asset_repo.get(asset_id)
if asset:
score, _ = score_asset(asset)
scored.append((asset_id, score))
else:
scored.append((asset_id, 0.0))
# 评分 + 随机噪声后按降序排列
scored.sort(
key=lambda x: x[1] + random.uniform(0.0, SCORE_RANDOM_NOISE_MAX),
reverse=True,
)
return [aid for aid, _ in scored]
def _fetch_asset_durations(self, asset_ids: List[str]) -> dict[str, float]:
"""从数据库获取素材时长信息.
File diff suppressed because one or more lines are too long
+7 -14
View File
@@ -1848,10 +1848,9 @@
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
@@ -1938,10 +1937,9 @@
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
@@ -3113,10 +3111,9 @@
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/dunder-proto": {
@@ -4457,10 +4454,9 @@
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
@@ -5008,10 +5004,9 @@
},
"node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -5024,10 +5019,9 @@
},
"node_modules/pretty-format/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
@@ -5735,10 +5729,9 @@
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/react-refresh": {
+1
View File
@@ -71,4 +71,5 @@ export interface VideoItem {
generation_params: Record<string, unknown>
download_url: string
generated_at: string
duplicate_rate?: number
}
+1 -2
View File
@@ -29,7 +29,6 @@ export function mapVideoToProductItem(video: VideoItem): ProductItem {
// 后端字段名为 generated_at,映射为 created_at 供前端统一使用
created_at: video.generated_at,
updated_at: video.generated_at,
// 后端 /videos 接口暂无 duplicate_rate 字段
duplicate_rate: undefined,
duplicate_rate: video.duplicate_rate,
}
}
+4 -1
View File
@@ -338,12 +338,15 @@ const GeneratePage: React.FC = () => {
size: titleSettings.size,
font: titleSettings.font,
color: titleSettings.color,
position: titleSettings.position as "top" | "center" | "bottom",
position: titleSettings.position as "top" | "center" | "bottom" | "custom",
bold: titleSettings.bold,
italic: titleSettings.italic,
stroke: titleSettings.stroke,
shadow: titleSettings.shadow,
posX: titleSettings.posX,
posY: titleSettings.posY,
}}
onTitlePositionChange={styleUpdaters.updateTitlePosition}
/>
)}
{currentStep >= 5 && generated && finalVideo && (
@@ -32,12 +32,15 @@ interface FrontendPreviewPlayerProps {
size: number
font: string
color: string
position: "top" | "center" | "bottom"
position: "top" | "center" | "bottom" | "custom"
bold?: boolean
italic?: boolean
stroke?: boolean
shadow?: boolean
posX?: number | null
posY?: number | null
}
onTitlePositionChange?: (posX: number, posY: number) => void
}
function formatTime(seconds: number): string {
@@ -105,6 +108,7 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
serverClips,
voiceAudioUrl,
titleSettings,
onTitlePositionChange,
}) => {
const segments = useMemo(
() => buildPlaybackSegments(assets, template, serverClips),
@@ -126,6 +130,69 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
return { width: 1080, height: 1920 }
}
})()
const customTitleXPct =
titleSettings?.posX != null && playRes.width > 0
? (titleSettings.posX / playRes.width) * 100
: null
const customTitleYPct =
titleSettings?.posY != null && playRes.height > 0
? (titleSettings.posY / playRes.height) * 100
: null
// ── 拖拽状态(用 ref 避免在每帧渲染中触发重渲染)──
const draggingTitleRef = useRef(false)
const titleDragRef = useRef<HTMLDivElement>(null)
const handleTitlePointerDown = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
if (!onTitlePositionChange || !playerContainerRef.current) return
e.preventDefault()
e.stopPropagation()
;(e.target as Element).setPointerCapture(e.pointerId)
draggingTitleRef.current = true
;(e.currentTarget as HTMLDivElement).style.cursor = "grabbing"
},
[onTitlePositionChange],
)
const handleTitlePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
if (!draggingTitleRef.current || !playerContainerRef.current) return
e.preventDefault()
e.stopPropagation()
// 拖拽过程中直接修改 DOM,不触发 React 渲染(避免频繁重渲染导致换行)
if (titleDragRef.current) {
const rect = playerContainerRef.current.getBoundingClientRect()
const relX = Math.max(0, Math.min(rect.width, e.clientX - rect.left))
const relY = Math.max(0, Math.min(rect.height, e.clientY - rect.top))
const xpct = (relX / rect.width) * 100
const ypct = (relY / rect.height) * 100
titleDragRef.current.style.left = `${xpct}%`
titleDragRef.current.style.top = `${ypct}%`
}
}, [])
const handleTitlePointerUp = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
if (!draggingTitleRef.current) return
draggingTitleRef.current = false
// 拖拽结束时才调用 onTitlePositionChange 保存最终位置
if (onTitlePositionChange && playerContainerRef.current) {
const rect = playerContainerRef.current.getBoundingClientRect()
const relX = Math.max(0, Math.min(rect.width, e.clientX - rect.left))
const relY = Math.max(0, Math.min(rect.height, e.clientY - rect.top))
const posX = Math.round((relX / rect.width) * playRes.width)
const posY = Math.round((relY / rect.height) * playRes.height)
onTitlePositionChange(posX, posY)
}
;(e.currentTarget as HTMLDivElement).style.cursor = "grab"
try {
if ((e.currentTarget as Element).hasPointerCapture(e.pointerId)) {
;(e.currentTarget as Element).releasePointerCapture(e.pointerId)
}
} catch {
/* ignore */
}
},
[onTitlePositionChange, playRes.width, playRes.height],
)
const playerContainerRef = useRef<HTMLDivElement>(null)
const [containerHeight, setContainerHeight] = useState(0)
useEffect(() => {
@@ -524,15 +591,36 @@ const FrontendPreviewPlayer: React.FC<FrontendPreviewPlayerProps> = ({
<div
style={{
position: "absolute",
left: `${titleSidePct}%`,
right: `${titleSidePct}%`,
textAlign: "center",
...(titleSettings.position === "top"
? { top: `${titleTopPct}%` }
: titleSettings.position === "center"
? { top: "50%", transform: "translateY(-50%)" }
: { bottom: `${titleBottomPct}%` }),
...(customTitleXPct != null && customTitleYPct != null
? {
left: `${customTitleXPct}%`,
top: `${customTitleYPct}%`,
transform: "translate(-50%, -50%)",
textAlign: "center" as const,
}
: {
left: `${titleSidePct}%`,
right: `${titleSidePct}%`,
textAlign: "center" as const,
...(titleSettings.position === "top"
? { top: `${titleTopPct}%` }
: titleSettings.position === "center"
? { top: "50%", transform: "translateY(-50%)" }
: { bottom: `${titleBottomPct}%` }),
}),
pointerEvents: "auto",
cursor: onTitlePositionChange ? "grab" : "default",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none",
padding: "8px 12px",
boxShadow: "inset 0 0 0 16px transparent",
}}
ref={titleDragRef}
onPointerDown={handleTitlePointerDown}
onPointerMove={handleTitlePointerMove}
onPointerUp={handleTitlePointerUp}
onPointerCancel={handleTitlePointerUp}
>
<span
style={{
@@ -1,5 +1,7 @@
/**
* 标题预设样式网格
* 双图层渲染:底层=描边轮廓(text-shadow模拟),上层=填充色
* 避免 -webkit-text-stroke 在 Chromium 中吞掉填充色的问题
*/
import React from "react"
import { getFontFamily } from "../../constants"
@@ -7,7 +9,10 @@ import { getFontFamily } from "../../constants"
interface TitlePresetItem {
key: string
label: string
previewStyle: React.CSSProperties
previewStyle: React.CSSProperties & {
_strokeColor?: string
_strokeWidth?: number
}
}
interface TitlePresetsGridProps {
@@ -17,6 +22,32 @@ interface TitlePresetsGridProps {
fontFamily?: string
}
/**
* 用 text-shadow 模拟描边轮廓(8方向 + 4对角 = 12层阴影)
*/
function buildStrokeShadow(color: string, width: number): string {
const w = width
const parts: string[] = []
// 4 cardinal directions
parts.push(`${w}px 0 ${color}`)
parts.push(`${-w}px 0 ${color}`)
parts.push(`0 ${w}px ${color}`)
parts.push(`0 ${-w}px ${color}`)
// 4 diagonal directions
const d = Math.round(w * 0.71 * 10) / 10 // 0.71 ≈ sqrt(2)/2
parts.push(`${d}px ${d}px ${color}`)
parts.push(`${-d}px ${d}px ${color}`)
parts.push(`${d}px ${-d}px ${color}`)
parts.push(`${-d}px ${-d}px ${color}`)
// 4 extra mid-points for smoother stroke
const h = Math.round(w * 0.5 * 10) / 10
parts.push(`${w}px ${h}px ${color}`)
parts.push(`${w}px ${-h}px ${color}`)
parts.push(`${-w}px ${h}px ${color}`)
parts.push(`${-w}px ${-h}px ${color}`)
return parts.join(", ")
}
const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({
presets,
activePreset,
@@ -27,18 +58,52 @@ const TitlePresetsGrid: React.FC<TitlePresetsGridProps> = ({
<div className="xx-title-presets-grid">
{presets.map((p) => {
const isActive = activePreset === p.key
const { _strokeColor, _strokeWidth, ...fillStyle } = p.previewStyle
const ff = getFontFamily(fontFamily || "思源黑体")
// 底层:描边轮廓(用 text-shadow 模拟粗描边)
const strokeStyle: React.CSSProperties = {
color: _strokeColor || "transparent",
textShadow:
_strokeColor && _strokeWidth
? buildStrokeShadow(_strokeColor, _strokeWidth)
: undefined,
fontWeight: fillStyle.fontWeight,
fontSize: fillStyle.fontSize,
lineHeight: 1,
}
// 上层:仅填充色 + 可选 textShadow(发光/投影效果)
const topStyle: React.CSSProperties = {
color: fillStyle.color,
textShadow: fillStyle.textShadow,
fontWeight: fillStyle.fontWeight,
fontSize: fillStyle.fontSize,
lineHeight: 1,
}
return (
<button
key={p.key}
className={`xx-title-preset-card${isActive ? " active" : ""}`}
onClick={() => onApply(p.key)}
title={p.label}
>
<span
className="xx-title-preset-preview-text"
style={{ ...p.previewStyle, fontFamily: getFontFamily(fontFamily || "思源黑体") }}
>
T
<span className="xx-title-preset-preview-text" style={{ position: "relative" }}>
{/* 底层:描边轮廓 */}
<span
aria-hidden
style={{
...strokeStyle,
fontFamily: ff,
position: "absolute",
top: 0,
left: 0,
}}
>
T
</span>
{/* 上层:填充色 */}
<span style={{ ...topStyle, fontFamily: ff, position: "relative" }}>T</span>
</span>
</button>
)
+219 -16
View File
@@ -42,6 +42,7 @@ export const POSITION_OPTIONS = [
{ value: "top", label: "顶部" },
{ value: "center", label: "居中" },
{ value: "bottom", label: "底部" },
{ value: "custom", label: "自定义" },
]
/* ── 标题字体选项 ── */
@@ -77,10 +78,11 @@ export const TITLE_PRESETS = [
label: "经典白字",
style: { size: 28, color: "#ffffff", bold: true, italic: false, stroke: true, shadow: false },
previewStyle: {
fontWeight: 700,
color: "#ffffff",
WebkitTextStroke: "1px #000000",
fontSize: "20px",
_strokeColor: "#000000",
_strokeWidth: 2,
fontWeight: 700,
fontSize: "32px",
},
},
{
@@ -88,45 +90,54 @@ export const TITLE_PRESETS = [
label: "黑金质感",
style: { size: 32, color: "#d4a843", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
fontWeight: 700,
color: "#d4a843",
fontWeight: 700,
fontSize: "32px",
textShadow: "1px 1px 3px rgba(0,0,0,0.8)",
fontSize: "20px",
},
},
{
key: "fresh_minimal",
label: "清新简约",
style: { size: 24, color: "#333333", bold: false, italic: false, stroke: false, shadow: false },
previewStyle: { fontWeight: 400, color: "#333333", fontSize: "18px" },
previewStyle: {
color: "#e8e8e8",
fontWeight: 400,
fontSize: "32px",
},
},
{
key: "variety_show",
label: "综艺花字",
style: { size: 36, color: "#ff4081", bold: true, italic: false, stroke: true, shadow: true },
previewStyle: {
fontWeight: 900,
color: "#ff4081",
WebkitTextStroke: "1.5px #ffffff",
_strokeColor: "#ffffff",
_strokeWidth: 2,
fontWeight: 900,
fontSize: "32px",
textShadow: "2px 2px 4px rgba(0,0,0,0.5)",
fontSize: "22px",
},
},
{
key: "business",
label: "商务极简",
style: { size: 24, color: "#1a1a1a", bold: false, italic: false, stroke: false, shadow: false },
previewStyle: { fontWeight: 400, color: "#1a1a1a", fontSize: "17px" },
previewStyle: {
color: "#e0e0e0",
fontWeight: 400,
fontSize: "32px",
},
},
{
key: "retro_film",
label: "复古胶片",
style: { size: 28, color: "#e8d5b7", bold: false, italic: false, stroke: false, shadow: true },
previewStyle: {
fontWeight: 400,
color: "#e8d5b7",
fontWeight: 400,
fontSize: "32px",
textShadow: "2px 2px 6px rgba(0,0,0,0.7)",
fontSize: "18px",
},
},
{
@@ -134,10 +145,10 @@ export const TITLE_PRESETS = [
label: "霓虹发光",
style: { size: 32, color: "#00e5ff", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
fontWeight: 700,
color: "#00e5ff",
fontWeight: 700,
fontSize: "32px",
textShadow: "0 0 4px #00e5ff, 0 0 8px #00e5ff, 0 0 16px rgba(0,229,255,0.5)",
fontSize: "20px",
},
},
{
@@ -145,10 +156,202 @@ export const TITLE_PRESETS = [
label: "手写字",
style: { size: 28, color: "#333333", bold: false, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#e0e0e0",
fontWeight: 400,
color: "#333333",
fontSize: "32px",
textShadow: "1px 1px 2px rgba(0,0,0,0.3)",
fontSize: "20px",
},
},
{
key: "outline_yellow",
label: "黄色描边",
style: { size: 28, color: "#ffd54f", bold: true, italic: false, stroke: true, shadow: false },
previewStyle: {
color: "#ffd54f",
_strokeColor: "#000000",
_strokeWidth: 2,
fontWeight: 700,
fontSize: "32px",
},
},
{
key: "outline_pink",
label: "粉色描边",
style: { size: 28, color: "#ff80ab", bold: true, italic: false, stroke: true, shadow: false },
previewStyle: {
color: "#ff80ab",
_strokeColor: "#000000",
_strokeWidth: 2,
fontWeight: 700,
fontSize: "32px",
},
},
{
key: "outline_blue",
label: "蓝色描边",
style: { size: 28, color: "#82b1ff", bold: true, italic: false, stroke: true, shadow: false },
previewStyle: {
color: "#82b1ff",
_strokeColor: "#000000",
_strokeWidth: 2,
fontWeight: 700,
fontSize: "32px",
},
},
{
key: "outline_green",
label: "绿色描边",
style: { size: 28, color: "#69f0ae", bold: true, italic: false, stroke: true, shadow: false },
previewStyle: {
color: "#69f0ae",
_strokeColor: "#000000",
_strokeWidth: 2,
fontWeight: 700,
fontSize: "32px",
},
},
{
key: "outline_gray",
label: "灰色描边",
style: { size: 28, color: "#bdbdbd", bold: true, italic: false, stroke: true, shadow: false },
previewStyle: {
color: "#bdbdbd",
_strokeColor: "#000000",
_strokeWidth: 2,
fontWeight: 700,
fontSize: "32px",
},
},
{
key: "bg_white",
label: "白底黑字",
style: { size: 28, color: "#1a1a1a", bold: true, italic: false, stroke: false, shadow: false },
previewStyle: {
color: "#1a1a1a",
fontWeight: 700,
fontSize: "32px",
background: "#ffffff",
borderRadius: "4px",
padding: "2px 6px",
},
},
{
key: "bg_yellow",
label: "黄底黑字",
style: { size: 28, color: "#1a1a1a", bold: true, italic: false, stroke: false, shadow: false },
previewStyle: {
color: "#1a1a1a",
fontWeight: 700,
fontSize: "32px",
background: "#ffd54f",
borderRadius: "4px",
padding: "2px 6px",
},
},
{
key: "bg_pink",
label: "粉底黑字",
style: { size: 28, color: "#1a1a1a", bold: true, italic: false, stroke: false, shadow: false },
previewStyle: {
color: "#1a1a1a",
fontWeight: 700,
fontSize: "32px",
background: "#ff80ab",
borderRadius: "4px",
padding: "2px 6px",
},
},
{
key: "bg_red",
label: "红底白字",
style: { size: 28, color: "#ffffff", bold: true, italic: false, stroke: false, shadow: false },
previewStyle: {
color: "#ffffff",
fontWeight: 700,
fontSize: "32px",
background: "#ef5350",
borderRadius: "4px",
padding: "2px 6px",
},
},
{
key: "neon_orange",
label: "橙色发光",
style: { size: 32, color: "#ff9100", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#ff9100",
fontWeight: 700,
fontSize: "32px",
textShadow: "0 0 4px #ff9100, 0 0 8px #ff9100, 0 0 16px rgba(255,145,0,0.5)",
},
},
{
key: "neon_purple",
label: "紫色发光",
style: { size: 32, color: "#d500f9", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#d500f9",
fontWeight: 700,
fontSize: "32px",
textShadow: "0 0 4px #d500f9, 0 0 8px #d500f9, 0 0 16px rgba(213,0,249,0.5)",
},
},
{
key: "bordered_white",
label: "白字绿框",
style: { size: 28, color: "#ffffff", bold: true, italic: false, stroke: false, shadow: false },
previewStyle: {
color: "#ffffff",
fontWeight: 700,
fontSize: "32px",
background: "#1a1a1a",
border: "2px solid #69f0ae",
borderRadius: "4px",
padding: "2px 6px",
},
},
{
key: "gradient_warm",
label: "暖色渐变",
style: { size: 32, color: "#ff6d00", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#ff6d00",
fontWeight: 700,
fontSize: "32px",
textShadow: "0 0 6px rgba(255,109,0,0.6), 1px 1px 2px rgba(0,0,0,0.5)",
},
},
{
key: "gradient_cool",
label: "冷色渐变",
style: { size: 32, color: "#00b0ff", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#00b0ff",
fontWeight: 700,
fontSize: "32px",
textShadow: "0 0 6px rgba(0,176,255,0.6), 1px 1px 2px rgba(0,0,0,0.5)",
},
},
{
key: "shadow_deep",
label: "深影白字",
style: { size: 28, color: "#ffffff", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#ffffff",
fontWeight: 700,
fontSize: "32px",
textShadow: "2px 2px 4px rgba(0,0,0,0.8), 0 0 8px rgba(0,0,0,0.4)",
},
},
{
key: "soft_gold",
label: "柔光金",
style: { size: 28, color: "#ffd54f", bold: true, italic: false, stroke: false, shadow: true },
previewStyle: {
color: "#ffd54f",
fontWeight: 700,
fontSize: "32px",
textShadow: "0 0 6px rgba(255,213,79,0.5), 1px 1px 2px rgba(0,0,0,0.4)",
},
},
]
+12 -12
View File
@@ -1733,36 +1733,36 @@
/* 标题预设卡片网格 */
.xx-title-presets-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
grid-template-columns: repeat(6, 52px);
gap: 1px;
}
.xx-title-preset-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
aspect-ratio: 1 / 1;
padding: 4px;
background: var(--bg-secondary);
width: 52px;
height: 52px;
padding: 0;
background: #404040;
border: 2px solid transparent;
border-radius: var(--radius-sm);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
text-align: center;
}
.xx-title-preset-card:hover {
border-color: var(--primary-200);
background: var(--bg-primary);
border-color: #666;
background: #4d4d4d;
}
.xx-title-preset-card.active {
border-color: var(--primary-color);
background: var(--primary-50);
border-color: #409eff;
background: #4d4d4d;
}
.xx-title-preset-preview-text {
font-size: 32px;
line-height: 1;
user-select: none;
}
@@ -20,13 +20,14 @@ const SECONDS_PER_ASSET = 15
/**
* 根据模板 segments 计算所需素材数量上限。
* 取每个 segment 的 duration_min 之和作为目标视频总时长,
* 再按 15 秒/素材估算需要多少个素材;结果钳制到 [1, 200] 区间(后端 limit 上限 200)。
* 再按 15 秒/素材估算需要多少个素材,且保证不少于片段数(每个片段至少 1 个素材);
* 结果钳制到 [1, 200] 区间(后端 limit 上限 200)。
*/
function computeLimitFromSegments(segments?: TemplateSegment[]): number {
if (!segments || segments.length === 0) return DEFAULT_LIMIT
const totalSeconds = segments.reduce((sum, seg) => sum + (seg.duration_min || 0), 0)
if (totalSeconds <= 0) return DEFAULT_LIMIT
const limit = Math.ceil(totalSeconds / SECONDS_PER_ASSET)
const limit = Math.max(segments.length, Math.ceil(totalSeconds / SECONDS_PER_ASSET))
return Math.max(1, Math.min(limit, 200))
}
@@ -233,7 +233,7 @@ export function useCanvasPlayer(
fontSize: number
fontFamily: string
color: string
position: "top" | "center" | "bottom"
position: "top" | "center" | "bottom" | "custom"
bold?: boolean
stroke?: boolean
shadow?: boolean
@@ -27,6 +27,8 @@ const DEFAULT_TITLE_SETTINGS: TitleSettings = {
stroke: true,
shadow: false,
color: "#ffffff",
posX: null,
posY: null,
}
export interface GenerateFormState {
@@ -117,6 +117,14 @@ export function useGenerateVideo(props: UseGenerateVideoProps) {
font_size: props.titleSettings.size,
font_color: props.titleSettings.color,
position: props.titleSettings.position,
...(props.titleSettings.position === "custom" &&
props.titleSettings.posX != null &&
props.titleSettings.posY != null
? {
pos_x: Math.round(props.titleSettings.posX),
pos_y: Math.round(props.titleSettings.posY),
}
: {}),
bold: props.titleSettings.bold,
stroke: props.titleSettings.stroke,
shadow: props.titleSettings.shadow,
@@ -46,7 +46,16 @@ export function useTitleStyleUpdaters({
const updatePosition = useCallback(
(position: string) => {
onTitleSettingsChange({ ...titleSettings, position })
// 切回三档预设时清掉自定义坐标
onTitleSettingsChange({ ...titleSettings, position, posX: null, posY: null })
},
[titleSettings, onTitleSettingsChange],
)
/** 拖拽更新自定义位置(由预览播放器调用) */
const updateTitlePosition = useCallback(
(posX: number, posY: number) => {
onTitleSettingsChange({ ...titleSettings, position: "custom", posX, posY })
},
[titleSettings, onTitleSettingsChange],
)
@@ -111,6 +120,7 @@ export function useTitleStyleUpdaters({
updateTitle,
toggleAiAutoSelect,
updatePosition,
updateTitlePosition,
updateFont,
updateSize,
updateColor,
+3
View File
@@ -16,6 +16,9 @@ export interface TitleSettings {
stroke: boolean
shadow: boolean
color: string
/** 自由位置坐标(PlayRes 像素),仅当 position="custom" 时有效 */
posX: number | null
posY: number | null
}
/* ── 智能匹配结果 ── */
@@ -221,7 +221,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
product.duplicateRate > 0 ? ` ${dupClass}` : ""
}`}
>
{product.duplicateRate > 0 ? `${product.duplicateRate.toFixed(1)}%` : "-"}
{product.duplicateRate != null ? `${product.duplicateRate.toFixed(1)}%` : "-"}
</span>
</div>
</div>
+18 -27
View File
@@ -91,8 +91,6 @@
height: 18px;
border: 2px solid var(--border-color);
border-radius: var(--radius-xs);
display: grid;
place-items: center;
transition: var(--transition-all);
background: var(--bg-primary);
flex-shrink: 0;
@@ -137,7 +135,7 @@
============================================================ */
.xx-products-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--space-md);
}
@@ -187,8 +185,6 @@
height: 22px;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: var(--radius-xs);
display: grid;
place-items: center;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(4px);
cursor: pointer;
@@ -262,11 +258,8 @@
.xx-product-thumb {
position: relative;
overflow: hidden;
display: grid;
place-items: center;
color: var(--text-inverse);
background: var(--color-gray-950);
max-height: 320px;
}
.xx-product-thumb-bg {
@@ -289,22 +282,28 @@
}
.xx-product-play {
position: relative;
z-index: 1;
width: 44px;
height: 44px;
border-radius: var(--radius-full);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
width: 52px;
height: 52px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
display: grid;
place-items: center;
font-size: var(--font-size-md);
transition: var(--transition-all);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #fff;
transition: all 0.2s;
cursor: pointer;
}
.xx-product-card:hover .xx-product-play {
background: var(--primary-color);
transform: scale(1.1);
transform: translate(-50%, -50%) scale(1.1);
}
/* 时长标签 */
@@ -324,7 +323,7 @@
/* 卡片信息区 */
.xx-product-info {
padding: 14px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: var(--space-sm);
@@ -506,8 +505,6 @@
z-index: 1000;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(8px);
display: grid;
place-items: center;
animation: player-fade-in 0.25s ease-out;
}
@@ -546,8 +543,6 @@
background: var(--color-gray-950);
aspect-ratio: 9 / 16;
max-height: 60vh;
display: grid;
place-items: center;
}
.xx-player-video-wrap video {
@@ -569,8 +564,6 @@
backdrop-filter: blur(4px);
color: var(--text-inverse);
font-size: 28px;
display: grid;
place-items: center;
cursor: pointer;
border: none;
transition: var(--transition-all);
@@ -667,8 +660,6 @@
backdrop-filter: blur(4px);
color: var(--text-inverse);
font-size: var(--font-size-md);
display: grid;
place-items: center;
cursor: pointer;
border: none;
transition: var(--transition-all);
+25 -11
View File
@@ -314,10 +314,13 @@ class VideoDeduplicator:
project_id: str,
current_video_id: str | None,
session: Session,
*,
user_id: str = "",
) -> float:
"""计算当前视频与项目内已有视频的最高相似度百分比。
"""计算当前视频与用户库内已有视频的最高相似度百分比。
遍历项目内所有其他有指纹的视频,对每个计算相似度:
优先按 user_id 全局比较(跨项目),user_id 为空时回退到项目级比较。
遍历最近 200 个其他有指纹的视频,对每个计算相似度:
- MD5 精确匹配 → 100%
- pHash 相似度 → (1.0 - avg_distance / 64) * 100
取最高值作为 duplicate_rate0~100)。
@@ -325,23 +328,34 @@ class VideoDeduplicator:
Args:
fingerprint: 当前视频的指纹
project_id: 项目 ID
project_id: 项目 IDuser_id 为空时的回退范围)
current_video_id: 当前视频 ID(排除自身,可为 None)
session: 数据库会话
user_id: 用户 ID(优先按用户全局比较)
Returns:
duplicate_rate: 0~100 的浮点数
"""
# 限制查询最近 100 个视频,避免大项目内存溢出
# 限制查询最近 200 个视频,避免大内存溢出
from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel
recent_models = (
session.query(GeneratedVideoModel)
.filter(GeneratedVideoModel.project_id == project_id)
.order_by(GeneratedVideoModel.generated_at.desc())
.limit(100)
.all()
)
# 优先按 user_id 全局比较(跨项目),否则回退到项目级
if user_id:
query = session.query(GeneratedVideoModel).filter(
GeneratedVideoModel.user_id == user_id,
)
logger.debug("compute_duplicate_rate: user-level scope user_id=%s", user_id)
else:
query = session.query(GeneratedVideoModel).filter(
GeneratedVideoModel.project_id == project_id,
)
logger.debug("compute_duplicate_rate: project-level fallback project_id=%s", project_id)
# 排除当前视频自身(记录可能已写入 DB,必须在查询层排除)
if current_video_id:
query = query.filter(GeneratedVideoModel.id != current_video_id)
recent_models = query.order_by(GeneratedVideoModel.generated_at.desc()).limit(200).all()
video_repo = SQLAlchemyGeneratedVideoRepository(session)
existing_videos = [video_repo._to_domain(m) for m in recent_models]
@@ -123,7 +123,13 @@ def create_video_record_and_dedup(
# 计算重复率百分比(与项目内所有已有视频对比取最高相似度)
try:
dup_rate = deduplicator.compute_duplicate_rate(fingerprint, project_id, video_id, session)
dup_rate = deduplicator.compute_duplicate_rate(
fingerprint,
project_id,
video_id,
session,
user_id=user_id,
)
generated_video.duplicate_rate = dup_rate
logger.info("Duplicate rate for %s: %.2f%%", video_id, dup_rate)
except Exception as rate_err:
@@ -200,6 +200,22 @@ class UnifiedRenderService:
# 3. 计算视频总时长(用于字幕显示时长)
video_duration = self._estimate_total_duration(layers)
# Debug: 输出各图层时长明细
for layer in layers:
layer_total = sum(UnifiedRenderService._clip_adjusted_duration(c) for c in layer.clips)
clip_details = [
f"{c.clip_id}(dur={c.duration:.3f},actual={c.actual_duration:.3f},speed={getattr(c, 'playback_speed', 1.0):.4f})"
for c in layer.clips
]
logger.info(
"[debug] layer=%s clips=%d total=%.3f transition_duration=%.3f details=%s",
layer.role,
len(layer.clips),
layer_total,
self.transition_duration,
", ".join(clip_details),
)
logger.info("[debug] estimated video_duration=%.3f", video_duration)
# 3.5 TTS 配音生成(如果配置了)
self._maybe_add_voiceover_layer(layers, video_duration=video_duration)
@@ -1422,6 +1438,7 @@ class UnifiedRenderService:
if trim_segments and len(trim_segments) > 1:
# 多段裁剪:展开为多个 clip
resolved_segments = TrimEngine.resolve_segments(trim_segments, actual_duration)
configured_speed = getattr(clip, "playback_speed", 1.0) or 1.0
for i, seg in enumerate(resolved_segments):
# 每个段生成一个独立的 ResolvedClip
seg_clip_id = f"{clip.id}_seg_{seg.segment_id}"
@@ -1429,6 +1446,19 @@ class UnifiedRenderService:
seg_start = seg.trim.start_time
seg_duration = seg.trim.duration
# 多段裁剪:如果段的时长超过素材实际时长,减速补偿
seg_speed = configured_speed
if actual_duration > 0 and seg_duration > actual_duration + 0.05:
seg_speed = max(0.25, round(configured_speed * actual_duration / seg_duration, 4))
logger.info(
"[debug] multi-seg clip=%s seg=%s duration=%.3f actual=%.3f → speed=%.4f",
clip.id,
seg.segment_id,
seg_duration,
actual_duration,
seg_speed,
)
rc = ResolvedClip(
clip_id=seg_clip_id,
asset_id=asset_id,
@@ -1439,7 +1469,7 @@ class UnifiedRenderService:
duration=seg_duration,
transition_effect=clip.transition_effect or "cut",
transition_duration=getattr(clip, "transition_duration", 0.0) or 0.0,
playback_speed=getattr(clip, "playback_speed", 1.0) or 1.0,
playback_speed=seg_speed,
config={**clip_config, "_segment_id": seg.segment_id},
actual_duration=actual_duration,
trim_config=seg.trim,
@@ -1461,6 +1491,7 @@ class UnifiedRenderService:
effective_trim: TrimConfig | None = None
final_start = clip.start_time
final_duration = clip.duration
configured_speed = getattr(clip, "playback_speed", 1.0) or 1.0
if trim_config is not None and actual_duration > 0:
effective_trim = trim_config.validate_and_resolve(actual_duration)
@@ -1474,6 +1505,25 @@ class UnifiedRenderService:
final_start = 0.0
final_duration = actual_duration
# 素材实际时长不足以覆盖配置的时长时,降低播放速度来补偿
# 例如:配置4s但素材只有3s → speed=0.75x,用满3s素材达到4s输出
if actual_duration > 0 and final_duration > actual_duration + 0.05:
compensated_speed = actual_duration / final_duration
# 保留用户设置的速度(如果已减速则叠加)
final_speed = configured_speed * compensated_speed
# 下限 0.25x
final_speed = max(0.25, round(final_speed, 4))
logger.info(
"[debug] clip=%s duration=%.3f actual=%.3f → 减速补偿 speed=%.4f (configured=%.3f)",
clip.id,
final_duration,
actual_duration,
final_speed,
configured_speed,
)
else:
final_speed = configured_speed
rc = ResolvedClip(
clip_id=clip.id,
asset_id=asset_id,
@@ -1484,13 +1534,25 @@ class UnifiedRenderService:
duration=final_duration,
transition_effect=clip.transition_effect or "cut",
transition_duration=getattr(clip, "transition_duration", 0.0) or 0.0,
playback_speed=getattr(clip, "playback_speed", 1.0) or 1.0,
playback_speed=final_speed,
config=clip_config,
actual_duration=actual_duration,
trim_config=effective_trim,
)
resolved.append(rc)
# Debug日志:记录每个clip的时长信息
eff_dur = _clip_effective_duration_pure(final_duration, actual_duration)
logger.info(
"[debug] resolved clip=%s duration=%.3f actual=%.3f effective=%.3f speed=%.4f start=%.3f",
clip.id,
final_duration,
actual_duration,
eff_dur,
final_speed,
final_start,
)
# 按 order 排序
resolved.sort(key=lambda c: c.order)
return resolved
@@ -1683,7 +1745,7 @@ class UnifiedRenderService:
if d > 0:
layer_dur = d
break
xfade_filter, _ = self._transition_engine.build_xfade_chain(
xfade_filter, xfade_estimated_dur = self._transition_engine.build_xfade_chain(
clip_durations=layer_durations,
clip_video_labels=layer_labels,
transitions=layer_transitions,
@@ -1692,6 +1754,13 @@ class UnifiedRenderService:
)
if xfade_filter:
filter_parts.append(xfade_filter)
logger.info(
"[unified-render] layer=%s xfade: clips=%d durations=%s estimated_dur=%.3f",
layer.role,
len(layer_labels),
[round(d, 3) for d in layer_durations],
xfade_estimated_dur,
)
layer_output_labels[layer.role] = out_label
# Step 3: 合成各层
@@ -1915,8 +1984,14 @@ class UnifiedRenderService:
def _clip_effective_duration(clip: ResolvedClip) -> float:
"""计算 clip 的有效时长(原速 trim 后时长)。
如果 playback_speed < 1(为补偿素材不足而减速),返回配置的 duration,
而非 min(duration, actual_duration)。
实际实现移至 packages.domain.render_layer_utils.clip_effective_duration。
"""
speed = getattr(clip, "playback_speed", 1.0) or 1.0
# 减速场景:duration 已通过降低 playback_speed 补偿,返回配置的 duration
if speed < 1.0 - 1e-6 and clip.duration > 0:
return clip.duration
return _clip_effective_duration_pure(clip.duration, clip.actual_duration)
# ── 画中画(PiP)相关方法 ──────────────────────────────────────────────────
+7
View File
@@ -14,6 +14,13 @@ from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
# 素材选取排序时注入的随机噪声上限(分)。
# score_asset 综合得分范围为 0-100,噪声 0~20 意味着:
# - 素材间得分差距 > 20 分时,排名不受影响(质量差异显著的素材保持稳定优先级)
# - 得分接近(差距 <= 20 分)的素材排名会随机浮动,使每次生成选出的素材组合不同,
# 从素材组合层面降低成片重复率;排名靠后的低分素材也有机会入选。
SCORE_RANDOM_NOISE_MAX = 20.0
@dataclass
class SmartMatchResult:
+5 -2
View File
@@ -150,8 +150,11 @@ def build_xfade_filter_chain(
else:
first_input_dur = cumulative - total_transition
# 原始 offset 计算
offset = max(0.0, cumulative - transition_duration * i)
# 正确的 offset 计算offset 应相对于累积输出时长
# offset = 累积输出中,转场开始的时间点
# = first_input_dur - transition_duration
# 这样每个转场之间的"纯内容"时长等于原始 clip 时长
offset = max(0.0, first_input_dur - transition_duration)
# 安全钳制:offset + td 不能超过第一个输入的时长
available = max(0.0, first_input_dur - offset)
+64
View File
@@ -119,6 +119,70 @@ class MediaKitClient:
return None
def detect_scene_changes(
self,
video_url: str,
max_frames: int = 20,
poll_interval: float = 2.0,
max_poll_attempts: int = 30,
) -> Optional[List[float]]:
"""检测视频场景切换点,返回时间戳列表.
降级策略:
1. 先尝试 SceneChange 策略
2. SceneChange 失败(OOM等)→ 退回 TimeInterval5秒间隔)
3. MediaKit 不可用 → 返回 None
Returns:
场景切换点时间戳列表,如 [0.0, 3.2, 7.8, 12.5]
失败返回 None
"""
if not self.is_available:
logger.warning("MediaKit 未配置,跳过场景检测")
return None
# 策略1:尝试 SceneChange
frames = self.extract_frames(
video_url=video_url,
strategy="SceneChange",
max_frames=max_frames,
poll_interval=poll_interval,
max_poll_attempts=max_poll_attempts,
)
# 策略2SceneChange 失败 → 退回 TimeInterval5秒间隔)
if frames is None:
logger.info("SceneChange 策略失败,降级为 TimeInterval5秒间隔)")
# 估算帧数:假设视频最长60秒,每5秒一帧
ti_max_frames = max(max_frames, 12)
frames = self.extract_frames(
video_url=video_url,
strategy="TimeInterval",
max_frames=ti_max_frames,
poll_interval=poll_interval,
max_poll_attempts=max_poll_attempts,
)
if frames is None:
return None
# 从帧列表中提取 timestamp,排序
timestamps = sorted({float(f.get("timestamp", 0.0)) for f in frames if "timestamp" in f})
if not timestamps:
return None
# 始终在列表开头加 0.0(素材起始点)
if timestamps[0] != 0.0:
timestamps.insert(0, 0.0)
logger.info(
"场景检测完成: video_url=%s scene_changes=%s",
video_url[:80],
timestamps,
)
return timestamps
def _submit_extract_task(
self,
video_url: str,
+3 -1
View File
@@ -24,8 +24,10 @@ celery==5.4.0
# 对象存储
oss2==2.18.4
# HTTP 客户端
# HTTP 客户端pin 间接依赖防止版本漂移)
httpx==0.27.2
httpcore==1.0.7
h2==4.1.0
# Prometheus monitoring
prometheus-client==0.21.1
+1
View File
@@ -15,5 +15,6 @@ pytest-xdist==3.6.1
diff-cover==8.0.3
# 资产质量评分依赖(与 requirements-worker.txt 保持一致)
numpy==1.26.4
scipy==1.13.1
Pillow==10.4.0
+6 -9
View File
@@ -28,15 +28,12 @@ if [ "$SKIP_BACKEND" = "true" ]; then
CONTEXTS=("CI/CD Pipeline / Frontend Lint (pull_request)")
else
CONTEXTS=(
"CI/CD Pipeline / Validate - Code Quality (pull_request)"
"CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)"
"CI/CD Pipeline / Validate - Migration (alembic) (pull_request)"
# CI 优化后 job 名称(2026-08):Code Quality 拆分为 Style+SecurityType Check+Migration 合并为 Python
# 与 pr_auto_scan.py 的 REQUIRED_CONTEXTS_APPROVE 保持一致
"CI/CD Pipeline / Validate - Style (pull_request)"
"CI/CD Pipeline / Validate - Security (pull_request)"
"CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request)"
"CI/CD Pipeline / Frontend Lint (pull_request)"
"CI/CD Pipeline / Unit Tests (pull_request)"
"CI/CD Pipeline / Frontend Unit Tests (pull_request)"
"CI/CD Pipeline / PR Build API Image (pull_request)"
"CI/CD Pipeline / PR Build Web Image (pull_request)"
"CI/CD Pipeline / PR Build Worker Image (pull_request)"
)
fi
@@ -153,4 +150,4 @@ done
echo
echo "⏰ 快速检查超时(2分钟),CI尚未完成,退出等待下次触发(workflow_run事件或5分钟定时扫描)"
exit 0
exit 0
+24
View File
@@ -4,6 +4,7 @@
# - docker-container driver, host 网络
# - 层缓存保存在 buildkit 容器及其 _state 命名卷中,job 结束不清理
# - 宿主机 ci-docker-cleanup.sh 已豁免该 builder
# - 每次执行自动同步宿主机 docker config 到 BuildKit 容器(确保 registry 认证)
# 用法: bash scripts/ci/ensure_persistent_builder.sh
set -eu
@@ -34,3 +35,26 @@ docker buildx use "$BUILDER"
docker buildx inspect "$BUILDER" --bootstrap
echo "✅ builder ready"
docker buildx ls | head -5
# === 同步宿主机 docker config 到 BuildKit 容器(确保 registry 认证) ===
# BuildKit 容器名遵循 docker buildx 命名规则: buildx_buildkit_<builder-name>_0
BUILDKIT_CONTAINER="buildx_buildkit_${BUILDER}_0"
if docker inspect "$BUILDKIT_CONTAINER" >/dev/null 2>&1; then
# 宿主机 docker config 路径
HOST_DOCKER_CONFIG="/root/.docker/config.json"
if [ -f "$HOST_DOCKER_CONFIG" ]; then
echo "=== 同步 docker config 到 BuildKit 容器 ==="
# 确保容器内 .docker 目录存在
docker exec "$BUILDKIT_CONTAINER" mkdir -p /root/.docker
# 拷贝 config.json
docker cp "$HOST_DOCKER_CONFIG" "$BUILDKIT_CONTAINER:/root/.docker/config.json"
echo "✅ docker config 已同步到 BuildKit 容器"
else
echo "⚠️ 宿主机 docker config 不存在: $HOST_DOCKER_CONFIG(跳过同步)"
fi
else
echo "⚠️ BuildKit 容器不存在: $BUILDKIT_CONTAINER(跳过 config 同步)"
fi
+4 -3
View File
@@ -303,9 +303,10 @@ def main():
"CI/CD Pipeline / CI Gate (pull_request)",
]
REQUIRED_CONTEXTS_APPROVE = [
"CI/CD Pipeline / Validate - Code Quality (pull_request)",
"CI/CD Pipeline / Validate - Type Check (mypy) (pull_request)",
"CI/CD Pipeline / Validate - Migration (alembic) (pull_request)",
# CI 优化后 job 名称(2026-08):Code Quality 拆分为 Style+SecurityType Check+Migration 合并为 Python
"CI/CD Pipeline / Validate - Style (pull_request)",
"CI/CD Pipeline / Validate - Security (pull_request)",
"CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request)",
"CI/CD Pipeline / Frontend Lint (pull_request)",
]
FRONTEND_ONLY_CONTEXT = [
+5 -5
View File
@@ -197,8 +197,8 @@ if [ "$USE_SHARED_PG" = "true" ]; then
# 创建主数据库(xdist 模式下各 worker 会创建自己的数据库,主库作为 fallback)
echo "创建主测试数据库: $CI_DB_NAME"
PGPASSWORD="$SHARED_PG_PASSWORD" python3 -c "
import psycopg2
conn = psycopg2.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
import psycopg
conn = psycopg.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
cur.execute(f'DROP DATABASE IF EXISTS \"$CI_DB_NAME\" WITH (FORCE)')
@@ -304,8 +304,8 @@ if [ "$USE_SHARED_PG" = "true" ]; then
# 清理所有以 CI_DB_NAME 开头的数据库(主库 + worker 库)
PGPASSWORD="${SHARED_PG_PASSWORD}" python3 -c "
import psycopg2
conn = psycopg2.connect(host='${SHARED_PG_HOST}', port=${SHARED_PG_PORT}, user='${SHARED_PG_USER}', password='${SHARED_PG_PASSWORD}', dbname='postgres')
import psycopg
conn = psycopg.connect(host='${SHARED_PG_HOST}', port=${SHARED_PG_PORT}, user='${SHARED_PG_USER}', password='${SHARED_PG_PASSWORD}', dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
@@ -344,4 +344,4 @@ python3 scripts/ci_coverage_summary.py
set -e
echo ""
echo "=== CI Integration Tests 全部通过 ✅ ==="
echo "=== CI Integration Tests 全部通过 ✅ ==="
+106 -47
View File
@@ -1,77 +1,136 @@
#!/bin/bash
# CI Unit Tests Job 主脚本
# 包含:依赖安装、增量测试选择、覆盖率测试、diff覆盖率门禁
# 包含:依赖缓存、增量测试选择、覆盖率测试、diff覆盖率门禁
set -eu
JOB_NAME="${1:-Unit Tests}"
echo "=== CI Unit Tests 开始 ==="
# --- 依赖缓存检查 ---
# 如果 requirements 文件未变化且依赖已安装,跳过 pip install(持久 runner 优化)
REQ_HASH_FILE="/tmp/.ci_unit_tests_req_hash"
CURRENT_REQ_HASH=""
if [ -f requirements-base.txt ] && [ -f requirements.txt ] && [ -f requirements-dev.txt ]; then
CURRENT_REQ_HASH=$(cat requirements-base.txt requirements.txt requirements-dev.txt | md5sum | cut -d' ' -f1)
fi
SKIP_PIP_INSTALL=false
if [ -n "$CURRENT_REQ_HASH" ] && [ -f "$REQ_HASH_FILE" ]; then
CACHED_HASH=$(cat "$REQ_HASH_FILE")
if [ "$CACHED_HASH" = "$CURRENT_REQ_HASH" ]; then
# 验证关键包是否还在
if python3 -c "import pytest; import celery" 2>/dev/null; then
echo "✅ 依赖无变化 (hash=$CURRENT_REQ_HASH),跳过 pip install"
SKIP_PIP_INSTALL=true
else
echo "⚠️ 依赖 hash 匹配但关键包缺失,重新安装"
fi
fi
fi
# --- 安装依赖 ---
echo ""
echo "=== 安装 Python 依赖 ==="
# pip install 带重试(网络不稳定时自动重试)
for i in 1 2 3; do
python3 -m pip install -q -r requirements-base.txt && break
echo "pip install requirements-base.txt 失败,重试 $i/3..."
[ $i -eq 3 ] && exit 1
sleep 5
done
for i in 1 2 3; do
python3 -m pip install -q -r requirements.txt && break
echo "pip install requirements.txt 失败,重试 $i/3..."
[ $i -eq 3 ] && exit 1
sleep 5
done
for i in 1 2 3; do
python3 -m pip install -q -r requirements-dev.txt && break
echo "pip install requirements-dev.txt 失败,重试 $i/3..."
[ $i -eq 3 ] && exit 1
sleep 5
done
if [ "$SKIP_PIP_INSTALL" = "false" ]; then
echo ""
echo "=== 安装 Python 依赖 ==="
# pip install 带重试(网络不稳定时自动重试),合并为一次调用减少开销
for i in 1 2 3; do
python3 -m pip install -q -r requirements-base.txt -r requirements.txt -r requirements-dev.txt && break
echo "pip install 失败,重试 $i/3..."
[ $i -eq 3 ] && exit 1
sleep 5
done
# 保存 hash 标记
if [ -n "$CURRENT_REQ_HASH" ]; then
echo "$CURRENT_REQ_HASH" > "$REQ_HASH_FILE"
fi
fi
pytest --version
# 双保险:确保numpy已安装
echo "=== 验证 numpy 安装 ==="
SKIP_NUMPY_TESTS=0
python3 -m pip install numpy==1.26.4 || {
echo " numpy 首次安装失败,尝试不使用缓存重新安装..."
python3 -m pip install --no-cache-dir numpy==1.26.4 || {
echo "⚠️ numpy 安装失败,跳过需要 numpy 的测试"
SKIP_NUMPY_TESTS=1
if python3 -c "import numpy; assert numpy.__version__ == '1.26.4'" 2>/dev/null; then
echo " numpy 1.26.4 已就绪(缓存命中)"
else
echo "需要安装 numpy 1.26.4..."
python3 -m pip install numpy==1.26.4 || {
echo "❌ numpy 首次安装失败,尝试不使用缓存重新安装..."
python3 -m pip install --no-cache-dir numpy==1.26.4 || {
echo "⚠️ numpy 安装失败,跳过需要 numpy 的测试"
SKIP_NUMPY_TESTS=1
}
}
}
fi
if [ "$SKIP_NUMPY_TESTS" = "0" ]; then
python3 -c "import numpy; print(f'✅ numpy {numpy.__version__} 安装成功')" || {
python3 -c "import numpy; print(f'✅ numpy {numpy.__version__} 就绪')" || {
echo "⚠️ numpy 导入失败,跳过需要 numpy 的测试"
SKIP_NUMPY_TESTS=1
}
fi
# --- 增量测试选择(PR ---
# --- 增量测试选择(PR + push 均支持 ---
UNIT_TEST_MODE="full"
SELECTED_TEST_FILES="tests/unit"
if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [ -n "${GITHUB_TOKEN:-}" ]; then
IS_PULL_REQUEST=false
IS_PUSH=false
[ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && IS_PULL_REQUEST=true
[ "${GITHUB_EVENT_NAME:-}" = "push" ] && IS_PUSH=true
if ($IS_PULL_REQUEST || $IS_PUSH) && [ -n "${GITHUB_TOKEN:-}" ]; then
echo ""
echo "=== 增量测试选择 ==="
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300"
CHANGED_FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" | python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin) if f['status'] != 'removed']")
CHANGED_FILES=""
if $IS_PULL_REQUEST; then
PR_NUMBER=$(echo "$GITHUB_REF" | sed 's|refs/pull/||; s|/.*||')
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?limit=300"
CHANGED_FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$API_URL" \
| python3 -c "import sys,json; [print(f['filename']) for f in json.load(sys.stdin) if f['status'] != 'removed']")
elif $IS_PUSH && [ -n "${GITHUB_SHA:-}" ]; then
# Push 事件:通过 GitHub API 获取本次 push 改动的文件
API_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}"
RESPONSE=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3.diff" "$API_URL" 2>/dev/null || echo "")
if [ -n "$RESPONSE" ]; then
CHANGED_FILES=$(echo "$RESPONSE" | grep '^diff --git' | sed 's|diff --git a/\(.*\) b/.*|\1|' || echo "")
fi
# 备用方案:获取 previous commit SHA 再查 API
if [ -z "$CHANGED_FILES" ]; then
PREV_SHA=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/commits?sha=${GITHUB_SHA}&per_page=2" \
| python3 -c "import sys,json; commits=json.load(sys.stdin); print(commits[1]['sha'] if len(commits)>1 else '')" 2>/dev/null || echo "")
if [ -n "$PREV_SHA" ]; then
COMPARE_URL="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/compare/${PREV_SHA}...${GITHUB_SHA}"
CHANGED_FILES=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" "$COMPARE_URL" \
| python3 -c "import sys,json; data=json.load(sys.stdin); [print(f['filename']) for f in data.get('files',[]) if f['status'] != 'removed']" 2>/dev/null || echo "")
fi
fi
fi
echo "改动文件数: $(echo "$CHANGED_FILES" | grep -c . || echo 0)"
set +e
CHANGED_FILES="$CHANGED_FILES" \
SELECTED_TESTS_OUTPUT=/tmp/selected_tests.txt \
python3 scripts/ci/select_unit_tests.py
SELECT_EXIT=$?
set -e
if [ $SELECT_EXIT -eq 0 ]; then
UNIT_TEST_MODE="incremental"
TEST_FILES=$(cat /tmp/selected_tests.txt | tr '\n' ' ')
SELECTED_TEST_FILES="$TEST_FILES"
echo "增量模式: $(cat /tmp/selected_tests.txt | wc -l) 个测试文件"
if [ -n "$CHANGED_FILES" ]; then
set +e
CHANGED_FILES="$CHANGED_FILES" \
SELECTED_TESTS_OUTPUT=/tmp/selected_tests.txt \
python3 scripts/ci/select_unit_tests.py
SELECT_EXIT=$?
set -e
if [ $SELECT_EXIT -eq 0 ]; then
UNIT_TEST_MODE="incremental"
TEST_FILES=$(cat /tmp/selected_tests.txt | tr '\n' ' ')
SELECTED_TEST_FILES="$TEST_FILES"
echo "增量模式: $(cat /tmp/selected_tests.txt | wc -l) 个测试文件"
else
echo "全量模式(增量选择失败)"
fi
else
echo "全量模式"
echo "无法获取改动文件列表,使用全量模式"
fi
fi
@@ -116,7 +175,7 @@ if [ "${GITHUB_EVENT_NAME:-}" = "pull_request" ] && [ -n "${GITHUB_TOKEN:-}" ];
PR_CODE_DIR="/tmp/pr-code-$$"
mkdir -p "$PR_CODE_DIR"
# 备份PR代码(含coverage.xmldiff-cover需要用到
# 备份PR代码(含coverage.xmldiff-cover需要用到
find . -maxdepth 1 -mindepth 1 ! -name 'diff_coverage.html' -exec cp -r {} "$PR_CODE_DIR/" \;
rm -rf .git
git init > /dev/null 2>&1
+4 -4
View File
@@ -409,8 +409,8 @@ except:
echo "创建测试数据库: $CI_DB_NAME"
PGPASSWORD="$SHARED_PG_PASSWORD" python3 -c "
import psycopg2
conn = psycopg2.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
import psycopg
conn = psycopg.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
cur.execute(f'DROP DATABASE IF EXISTS \"$CI_DB_NAME\" WITH (FORCE)')
@@ -431,8 +431,8 @@ conn.close()
# 清理数据库
echo "清理测试数据库: $CI_DB_NAME"
PGPASSWORD="$SHARED_PG_PASSWORD" python3 -c "
import psycopg2
conn = psycopg2.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
import psycopg
conn = psycopg.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
cur.execute(f'DROP DATABASE IF EXISTS \"$CI_DB_NAME\" WITH (FORCE)')
-103
View File
@@ -1,103 +0,0 @@
#!/bin/bash
# ============================================
# 基础镜像同步脚本 - 从公共镜像源同步到私有ACR
# 用法:
# ACR_USERNAME=xxx ACR_PASSWORD=yyy bash scripts/ci/sync_base_images.sh
# ============================================
set -euo pipefail
ACR_REGISTRY="${ACR_REGISTRY:-xiaoxia-registry.cn-hangzhou.cr.aliyuncs.com/xiaoxiakeji}"
ACR_USERNAME="${ACR_USERNAME:-}"
ACR_PASSWORD="${ACR_PASSWORD:-}"
SOURCE_PREFIX="${SOURCE_PREFIX:-docker.m.daocloud.io/library}"
# 需要同步的镜像列表 (源镜像名:tag => ACR目标名:tag)
IMAGES=(
"python:3.12-slim-bookworm"
"python:3.12-slim"
"node:20"
"nginx:alpine"
)
echo "============================================"
echo " 基础镜像同步到 ACR"
echo " ACR: $ACR_REGISTRY"
echo " 源: $SOURCE_PREFIX"
echo "============================================"
echo ""
# 登录 ACR
if [ -n "$ACR_PASSWORD" ] && [ -n "$ACR_USERNAME" ]; then
echo "登录 ACR..."
ACR_HOST=$(echo "$ACR_REGISTRY" | cut -d/ -f1)
printf '%s' "$ACR_PASSWORD" | docker login "$ACR_HOST" -u "$ACR_USERNAME" --password-stdin
echo "ACR 登录成功"
echo ""
fi
success=0
failed=0
for image in "${IMAGES[@]}"; do
source_image="${SOURCE_PREFIX}/${image}"
target_image="${ACR_REGISTRY}/base/${image}"
echo "--- 同步: $image ---"
echo " 源: $source_image"
echo " 目标: $target_image"
# Pull 源镜像(带重试)
pulled=0
for attempt in 1 2 3; do
echo " Pull 尝试 $attempt/3..."
if docker pull "$source_image"; then
pulled=1
break
fi
echo " Pull 失败,5s 后重试..."
sleep 5
done
if [ "$pulled" -eq 0 ]; then
echo " ❌ Pull 失败: $image"
failed=$((failed + 1))
continue
fi
# Tag
docker tag "$source_image" "$target_image"
echo " Tag 完成"
# Push 到 ACR
pushed=0
for attempt in 1 2 3; do
echo " Push 尝试 $attempt/3..."
if docker push "$target_image"; then
pushed=1
break
fi
echo " Push 失败,5s 后重试..."
sleep 5
done
if [ "$pushed" -eq 1 ]; then
echo " ✅ 同步成功: $image"
success=$((success + 1))
else
echo " ❌ Push 失败: $image"
failed=$((failed + 1))
fi
echo ""
done
echo "============================================"
echo " 同步完成"
echo " 成功: $success"
echo " 失败: $failed"
echo "============================================"
if [ "$failed" -gt 0 ]; then
exit 1
fi
-157
View File
@@ -1,157 +0,0 @@
#!/bin/bash
# CI Validate: 代码质量与安全扫描(并行Job 1/3)
# 包含:密钥扫描、格式检查、安全扫描、依赖漏洞、死代码检测、脚本语法校验
set -eu
echo "=== CI Validate: 代码质量与安全扫描 ==="
# --- 密钥检测 ---
echo ""
echo "=== [1/6] Secret detection (detect-secrets) ==="
python3 -m pip install -q detect-secrets
detect-secrets --version
detect-secrets scan \
--all-files \
--exclude-files '(^|/)(tests|test|e2e|__tests__|spec|docs|node_modules|site-packages|migrations|alembic|.gitea|.git|.pytest_cache|.next|dist|build)/' \
--exclude-files '\.(md|rst|txt|lock|example|sample|min\.js|min\.css|spec\.ts|test\.ts|test\.py)$' \
--exclude-files '(package-lock|yarn\.lock|poetry\.lock|Pipfile\.lock)$' \
--disable-plugin Base64HighEntropyString \
--disable-plugin HexHighEntropyString \
--disable-plugin BasicAuthDetector \
--disable-plugin KeywordDetector \
--disable-plugin IPPublicDetector \
> /tmp/secrets-scan.json 2>&1
FOUND=$(python3 -c "
import json
try:
with open('/tmp/secrets-scan.json') as f:
data = json.load(f)
results = data.get('results', {})
total = sum(len(v) for v in results.values())
print(total)
except Exception:
print('error')
")
echo "Secrets detected: $FOUND"
if [ "$FOUND" != "0" ] && [ "$FOUND" != "error" ]; then
echo ""
echo "=== Secret details ==="
python3 -c "
import json
with open('/tmp/secrets-scan.json') as f:
data = json.load(f)
for fpath, items in data.get('results', {}).items():
for item in items:
line = item.get('line_number', '?')
stype = item.get('type', '?')
hashed = item.get('hashed_secret', '')[:16]
print(f' {fpath}:{line} [{stype}] {hashed}...')
"
echo ""
echo "ERROR: Potential secrets detected in code!"
exit 1
fi
echo "✅ Secret scan passed"
# --- 代码质量检查(全量,PR 和 push 统一标准)---
# 历史:PR 侧用增量检查以加速,但会导致 push 侧全量检查失败时 PR 侧感知不到
# 现在统一全量检查,确保 CI 真正保护主分支(black/isort/ruff 全量仅多几十秒)
echo ""
echo "=== [2/6] Code quality checks (full scan) ==="
SCAN_MODE="full"
echo "Full scan mode"
python3 -m compileall -q alembic apps packages tests scripts
python3 -m black --check --fast alembic apps packages tests scripts
python3 -m isort --check-only alembic apps packages tests scripts
python3 -m ruff check apps packages tests --statistics
echo "✅ Code quality checks passed"
# --- Bandit 安全扫描(仅告警) ---
echo ""
echo "=== [3/6] Security scan (bandit, advisory only) ==="
set +e
bandit -r apps packages -q -ll
BANDIT_EXIT=$?
set -e
if [ "$BANDIT_EXIT" -ne 0 ]; then
echo "⚠️ Bandit found security issues (advisory mode - not blocking CI)"
else
echo "✅ Bandit security scan passed"
fi
# --- Pip-audit 依赖漏洞扫描(仅告警) ---
echo ""
echo "=== [4/6] Python dependency vulnerability scan (pip-audit, advisory only) ==="
python3 -m pip install -q pip-audit
pip-audit --version
EXIT_CODE=0
for req_file in requirements.txt requirements-base.txt requirements-dev.txt; do
if [ -f "$req_file" ]; then
echo "--- Scanning $req_file ---"
pip-audit -r "$req_file" --desc on 2>&1 | head -40 || EXIT_CODE=$?
echo ""
fi
done
echo "pip-audit scan completed (advisory mode - warnings only, not blocking CI)"
# --- Vulture 死代码检测(仅告警) ---
echo ""
echo "=== [5/6] Dead code detection (vulture, advisory only) ==="
set +e
python3 -m pip install -q vulture
vulture --version
echo "告警模式,不阻断CI。置信度>=90%建议尽快确认。"
echo ""
vulture apps packages scripts \
--exclude "tests,test,migrations,.gitea,docs,node_modules,site-packages,*/test_*.py,*/conftest.py" \
--min-confidence 70 \
2>&1 | sort -t'(' -k2 -rn | head -80
echo ""
echo "=== vulture scan summary ==="
echo "发现潜在死代码(可能包含框架装饰器注册的函数,为误报)"
echo "建议:定期人工审查高置信度(>=90%)条目"
set -e
# --- CI脚本语法校验 ---
echo ""
echo "=== [6/6] CI & shell scripts syntax validation ==="
SYNTAX_ERROR=0
# 检查所有 CI shell 脚本
for script in scripts/ci/*.sh; do
if [ -f "$script" ]; then
if ! bash -n "$script" 2>&1; then
echo "❌ 语法错误: $script"
SYNTAX_ERROR=1
fi
fi
done
# 检查所有 CI Python 脚本语法
for script in scripts/ci/*.py; do
if [ -f "$script" ]; then
if ! python3 -m py_compile "$script" 2>&1; then
echo "❌ Python语法错误: $script"
SYNTAX_ERROR=1
fi
fi
done
# 检查 .gitea/workflows 下的脚本(如果有)
for script in .gitea/workflows/*.sh; do
if [ -f "$script" ]; then
if ! bash -n "$script" 2>&1; then
echo "❌ 语法错误: $script"
SYNTAX_ERROR=1
fi
fi
done
if [ "$SYNTAX_ERROR" -ne 0 ]; then
echo "❌ CI脚本语法校验失败,见上方错误"
exit 1
fi
echo "✅ All CI scripts syntax OK"
echo ""
echo "=== CI Validate: 代码质量与安全扫描 全部通过 ✅ ==="
+4 -4
View File
@@ -169,8 +169,8 @@ if [ "$USE_SHARED_PG" = "true" ]; then
echo "创建测试数据库: $CI_DB_NAME"
PGPASSWORD="$SHARED_PG_PASSWORD" python3 -c "
import psycopg2
conn = psycopg2.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
import psycopg
conn = psycopg.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
cur.execute(f'DROP DATABASE IF EXISTS \"$CI_DB_NAME\" WITH (FORCE)')
@@ -185,8 +185,8 @@ conn.close()
echo ""
echo "清理测试数据库: $CI_DB_NAME"
PGPASSWORD="$SHARED_PG_PASSWORD" python3 -c "
import psycopg2
conn = psycopg2.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
import psycopg
conn = psycopg.connect(host='$SHARED_PG_HOST', port=$SHARED_PG_PORT, user='$SHARED_PG_USER', password='$SHARED_PG_PASSWORD', dbname='postgres')
conn.autocommit = True
cur = conn.cursor()
cur.execute(f'DROP DATABASE IF EXISTS \"$CI_DB_NAME\" WITH (FORCE)')
+1 -1
View File
@@ -397,7 +397,7 @@ fi
echo "Stopping old containers..."
# 优雅关闭:先 stop(发 SIGTERM,等待),再 rm
# Worker 需要更长时间(视频任务最长可能5分钟)
docker stop -t 300 xiaoxia-worker-staging 2>/dev/null || true
docker stop -t 120 xiaoxia-worker-staging 2>/dev/null || true
docker stop -t 30 xiaoxia-api-staging 2>/dev/null || true
docker stop -t 10 xiaoxia-web-staging 2>/dev/null || true
docker rm xiaoxia-worker-staging xiaoxia-api-staging xiaoxia-web-staging 2>/dev/null || true
+1 -1
View File
@@ -187,7 +187,7 @@ health_check() {
return 0
fi
sleep 5
sleep 3
done
# 超时了
+5 -5
View File
@@ -197,14 +197,14 @@ class TestComputeAssetAvailability:
def test_large_gap_remains_usable(self):
"""区间之间留有 ≥3s 空闲段(扩边后仍 ≥3s)→ usable=True。"""
# [0,2] 扩边到 [0,2.3][5.3,10] 扩边前为 [5,10] 扩边起 4.7;空闲 [2.3,4.7]=2.4s <3
# 改用更大间隙:[0,2] 与 [6,10],扩边后空闲 [2.3,5.7]=3.4s ≥3
# [0,2] 扩边到 [0,3.5][9,10] 扩边到 [7.5,10];空闲 [3.5,7.5]=4.0s >=3
# 使用 [0,2] 与 [9,10],扩边后空闲 [3.5,7.5]=4.0s ≥3 → usable
info = compute_asset_availability(
_make_asset(
duration=10.0,
ranges=[
_range(0.0, 2.0, use_count=MAX_RANGE_USE_COUNT),
_range(6.0, 10.0, use_count=MAX_RANGE_USE_COUNT),
_range(9.0, 10.0, use_count=MAX_RANGE_USE_COUNT),
],
)
)
@@ -236,8 +236,8 @@ class TestComputeAssetAvailability:
assert info["usable"] is True
def test_segment_edge_gap_constant(self):
"""边缘间隙常量为 0.3s(与 MediaKit 冲突检测同口径)。"""
assert SEGMENT_EDGE_GAP == 0.3
"""边缘间隙常量为 1.5s(与 MediaKit 冲突检测同口径)。"""
assert SEGMENT_EDGE_GAP == 1.5
def test_domain_entity_metadata_dict_form(self):
"""领域实体形态(metadata 为 dict,无 classification_result)也能读到区间。
+10 -10
View File
@@ -124,9 +124,9 @@ def _ranges(db, aid="a1"):
def test_config_constants():
assert MAX_RANGE_USE_COUNT == 3
assert REUSE_RATIO_LIMIT == 0.15
assert SEGMENT_EDGE_GAP == 0.3
assert MAX_RANGE_USE_COUNT == 2
assert REUSE_RATIO_LIMIT == 0.10
assert SEGMENT_EDGE_GAP == 1.5
# ── get_used_segments ─────────────────────────────────────────────────────────
@@ -320,13 +320,13 @@ def test_find_reusable_prefers_oldest_unused(patched_model):
def test_find_reusable_excludes_max_use_count(patched_model):
"""use_count 达到上限(3)的区间不再参与复用;全部达上限返回 None。"""
"""use_count 达到上限(2)的区间不再参与复用;全部达上限返回 None。"""
models = {
"a1": FakeModel(
"a1",
{
"used_time_ranges": [
{"start": 0.0, "end": 10.0, "use_count": 3, "last_used_at": "2026-01-01T00:00:00"},
{"start": 0.0, "end": 10.0, "use_count": 2, "last_used_at": "2026-01-01T00:00:00"},
]
},
)
@@ -335,22 +335,22 @@ def test_find_reusable_excludes_max_use_count(patched_model):
assert find_reusable_range(db, "a1", 5.0, 30.0) is None
def test_find_reusable_fourth_use_rejected(patched_model):
"""同区间复用第 4 次被拒绝:use_count=2 的可复用,use_count=3 的不可复用。"""
def test_find_reusable_third_use_rejected(patched_model):
"""同区间复用第 3 次被拒绝:use_count=1 的可复用,use_count=2 的不可复用。"""
models = {
"a1": FakeModel(
"a1",
{
"used_time_ranges": [
{"start": 0.0, "end": 10.0, "use_count": 2, "last_used_at": "2026-03-01T00:00:00"},
{"start": 10.0, "end": 20.0, "use_count": 3, "last_used_at": "2026-01-01T00:00:00"},
{"start": 0.0, "end": 10.0, "use_count": 1, "last_used_at": "2026-03-01T00:00:00"},
{"start": 10.0, "end": 20.0, "use_count": 2, "last_used_at": "2026-01-01T00:00:00"},
]
},
)
}
db = _db(models)
result = find_reusable_range(db, "a1", 5.0, 30.0)
# 只能选 use_count=2 的区间(start=0),不能选 use_count=3 的(虽然它更老)
# 只能选 use_count=1 的区间(start=0),不能选 use_count=2 的(虽然它更老)
assert result is not None and result[0] == 0.0
+152
View File
@@ -0,0 +1,152 @@
"""测试 create_video_record_and_dedup 传递 user_id 到查重逻辑.
验证 P0 修复:查重范围从项目级扩大到用户级。
dedup_helpers 必须把 user_id 传给 compute_duplicate_rate。
"""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
# Mock cv2/numpy before imports
sys.modules.setdefault("cv2", MagicMock())
sys.modules.setdefault("numpy", MagicMock())
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "apps" / "api"))
sys.path.insert(0, str(ROOT / "packages"))
sys.path.insert(0, str(ROOT / "apps" / "worker"))
import os
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret")
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
class TestDedupHelpersUserIdPassthrough:
"""验证 dedup_helpers 把 user_id 传递给 compute_duplicate_rate."""
def test_user_id_passed_to_compute_duplicate_rate(self):
"""create_video_record_and_dedup 必须传 user_id 给 compute_duplicate_rate."""
from video_processing.dedup_helpers import create_video_record_and_dedup
session = MagicMock()
mock_video_repo = MagicMock()
mock_fingerprint = MagicMock()
mock_fingerprint.to_dict.return_value = {"md5": "test", "keyframe_phashes": ["aa"]}
mock_deduplicator = MagicMock()
mock_deduplicator.compute_fingerprint.return_value = mock_fingerprint
mock_deduplicator.check_duplicate.return_value = None
mock_deduplicator.compute_duplicate_rate.return_value = 42.5
with (
patch(
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
return_value=mock_video_repo,
),
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
):
result = create_video_record_and_dedup(
generation_task_id="task-001",
project_id="proj-001",
user_id="user-abc",
batch_id="",
file_url="https://example.com/video.mp4",
file_size=1024,
duration=15.0,
video_path="/tmp/fake_video.mp4",
mode="smart",
session=session,
)
# 验证 compute_duplicate_rate 被调用且 user_id 正确传递
mock_deduplicator.compute_duplicate_rate.assert_called_once()
call_kwargs = mock_deduplicator.compute_duplicate_rate.call_args
assert (
call_kwargs.kwargs.get("user_id") == "user-abc"
), f"user_id 应传递给 compute_duplicate_rate,实际: {call_kwargs}"
def test_empty_user_id_still_works(self):
"""user_id 为空时仍然正常执行(回退到 project 级比较)."""
from video_processing.dedup_helpers import create_video_record_and_dedup
session = MagicMock()
mock_video_repo = MagicMock()
mock_fingerprint = MagicMock()
mock_fingerprint.to_dict.return_value = {"md5": "test"}
mock_deduplicator = MagicMock()
mock_deduplicator.compute_fingerprint.return_value = mock_fingerprint
mock_deduplicator.check_duplicate.return_value = None
mock_deduplicator.compute_duplicate_rate.return_value = 0.0
with (
patch(
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
return_value=mock_video_repo,
),
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
):
result = create_video_record_and_dedup(
generation_task_id="task-002",
project_id="proj-002",
user_id="",
batch_id="",
file_url="https://example.com/video.mp4",
file_size=1024,
duration=10.0,
video_path="/tmp/fake.mp4",
mode="smart",
session=session,
)
mock_deduplicator.compute_duplicate_rate.assert_called_once()
call_kwargs = mock_deduplicator.compute_duplicate_rate.call_args
assert call_kwargs.kwargs.get("user_id") == ""
def test_duplicate_rate_saved_to_video_record(self):
"""compute_duplicate_rate 的返回值应写入 generated_video.duplicate_rate."""
from video_processing.dedup_helpers import create_video_record_and_dedup
session = MagicMock()
mock_video_repo = MagicMock()
mock_fingerprint = MagicMock()
mock_fingerprint.to_dict.return_value = {"md5": "test"}
mock_deduplicator = MagicMock()
mock_deduplicator.compute_fingerprint.return_value = mock_fingerprint
mock_deduplicator.check_duplicate.return_value = None
mock_deduplicator.compute_duplicate_rate.return_value = 78.5
with (
patch(
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
return_value=mock_video_repo,
),
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
):
result = create_video_record_and_dedup(
generation_task_id="task-003",
project_id="proj-003",
user_id="user-xyz",
batch_id="",
file_url="https://example.com/v.mp4",
file_size=2048,
duration=20.0,
video_path="/tmp/fake2.mp4",
mode="smart",
session=session,
)
# 验证 update 被调用(包含 duplicate_rate 的记录)
mock_video_repo.update.assert_called_once()
updated_video = mock_video_repo.update.call_args[0][0]
assert updated_video.duplicate_rate == 78.5
+93 -15
View File
@@ -56,9 +56,10 @@ class TestComputeDuplicateRate:
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = (
[]
)
query_mock = MagicMock()
query_mock.filter.return_value = query_mock
query_mock.order_by.return_value.limit.return_value.all.return_value = []
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)
assert rate == 0.0
@@ -73,7 +74,6 @@ class TestComputeDuplicateRate:
session = MagicMock()
existing = self._make_existing_video("existing1", {"md5": "exact_match_md5", "keyframe_phashes": ["aa"]})
# Create a mock model with the domain attributes
mock_model = MagicMock(spec=GeneratedVideoModel)
mock_model.id = existing.id
mock_model.project_id = existing.project_id
@@ -83,10 +83,12 @@ class TestComputeDuplicateRate:
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
mock_repo._to_domain.return_value = existing
# Mock the session.query chain
session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
mock_model
]
# 链式 filter: 第一次 scope filter,第二次 self-exclusion filter
# 让 filter() 返回的对象仍然支持 order_by() 链
query_mock = MagicMock()
query_mock.filter.return_value = query_mock # filter → filter chainable
query_mock.order_by.return_value.limit.return_value.all.return_value = [mock_model]
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)
assert rate == 100.0
@@ -113,9 +115,10 @@ class TestComputeDuplicateRate:
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
mock_repo._to_domain.return_value = existing
session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
mock_model
]
query_mock = MagicMock()
query_mock.filter.return_value = query_mock
query_mock.order_by.return_value.limit.return_value.all.return_value = [mock_model]
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)
# hamming distance = 2, similarity = (1 - 2/64) * 100 = 96.875
@@ -140,9 +143,10 @@ class TestComputeDuplicateRate:
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
mock_repo._to_domain.return_value = self_video
session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
mock_model
]
query_mock = MagicMock()
query_mock.filter.return_value = query_mock
query_mock.order_by.return_value.limit.return_value.all.return_value = [mock_model]
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)
assert rate == 0.0
@@ -172,15 +176,89 @@ class TestComputeDuplicateRate:
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
mock_repo._to_domain.side_effect = [existing1, existing2]
session.query.return_value.filter.return_value.order_by.return_value.limit.return_value.all.return_value = [
query_mock = MagicMock()
query_mock.filter.return_value = query_mock
query_mock.order_by.return_value.limit.return_value.all.return_value = [
mock_model1,
mock_model2,
]
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session)
# max similarity: e2 distance=1, (1-1/64)*100 = 98.4375
assert rate == pytest.approx(98.44, abs=0.1)
def test_user_id_scope_cross_project(self):
"""传 user_id 时应跨项目查询,而非仅当前项目."""
from video_processing.dedup import VideoDeduplicator
from packages.adapters.sqlalchemy_impl.models import GeneratedVideoModel
deduplicator = VideoDeduplicator()
fingerprint = self._make_fingerprint(md5="cross_proj_md5")
session = MagicMock()
# 模拟一个不同项目但同一用户的视频
existing = self._make_existing_video(
"existing_other_proj", {"md5": "cross_proj_md5", "keyframe_phashes": ["aa"]}
)
existing.project_id = "proj2" # 不同项目
existing.user_id = "user1"
mock_model = MagicMock(spec=GeneratedVideoModel)
mock_model.id = existing.id
mock_model.project_id = existing.project_id
mock_model.user_id = existing.user_id
mock_model.video_fingerprint = existing.video_fingerprint
mock_model.generated_at = "2026-01-01"
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
mock_repo._to_domain.return_value = existing
query_mock = MagicMock()
query_mock.filter.return_value = query_mock
query_mock.order_by.return_value.limit.return_value.all.return_value = [mock_model]
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(
fingerprint,
"proj1",
"vid1",
session,
user_id="user1",
)
# 应通过 user_id 过滤,且匹配到跨项目视频
assert rate == 100.0
def test_user_id_empty_falls_back_to_project(self):
"""user_id 为空时应回退到 project_id 过滤."""
from video_processing.dedup import VideoDeduplicator
deduplicator = VideoDeduplicator()
fingerprint = self._make_fingerprint()
session = MagicMock()
with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo:
mock_repo = MockRepo.return_value
query_mock = MagicMock()
query_mock.filter.return_value = query_mock
query_mock.order_by.return_value.limit.return_value.all.return_value = []
session.query.return_value = query_mock
rate = deduplicator.compute_duplicate_rate(
fingerprint,
"proj1",
"vid1",
session,
user_id="",
)
assert rate == 0.0
# 验证使用的是 project_id 过滤(回退路径)
# 通过检查 filter 被调用时的参数来间接验证
class TestDuplicateRateAPI:
"""Test that duplicate_rate is returned in API responses."""
+97
View File
@@ -0,0 +1,97 @@
"""Tests for duration compensation when source video is shorter than configured duration."""
from pathlib import Path
import pytest
from apps.worker.video_processing.unified_render_service import ResolvedClip, UnifiedRenderService
class TestClipEffectiveDurationWithSpeedCompensation:
"""Test _clip_effective_duration handles speed < 1 correctly."""
def test_normal_speed_returns_min(self):
"""When speed=1.0, effective_duration = min(duration, actual_duration)."""
clip = ResolvedClip(
clip_id="c1",
asset_id="a1",
local_path=Path("/tmp/fake.mp4"),
clip_type="main",
order=0,
start_time=0.0,
duration=4.0,
actual_duration=3.0, # shorter than configured
playback_speed=1.0,
)
# Without speed compensation, effective = min(4, 3) = 3
assert UnifiedRenderService._clip_effective_duration(clip) == 3.0
def test_compensated_speed_returns_configured_duration(self):
"""When speed < 1 (compensated), effective_duration = configured duration."""
clip = ResolvedClip(
clip_id="c1",
asset_id="a1",
local_path=Path("/tmp/fake.mp4"),
clip_type="main",
order=0,
start_time=0.0,
duration=4.0,
actual_duration=3.0, # shorter than configured
playback_speed=0.75, # compensated: 3/4 = 0.75
)
# With speed compensation, effective = configured duration = 4.0
assert UnifiedRenderService._clip_effective_duration(clip) == 4.0
def test_zero_actual_duration_returns_configured(self):
"""When actual_duration=0, effective_duration = configured duration."""
clip = ResolvedClip(
clip_id="c1",
asset_id="a1",
local_path=Path("/tmp/fake.mp4"),
clip_type="main",
order=0,
start_time=0.0,
duration=4.0,
actual_duration=0.0,
playback_speed=1.0,
)
assert UnifiedRenderService._clip_effective_duration(clip) == 4.0
def test_compensated_speed_with_actual_zero(self):
"""When speed < 1 and actual=0, still returns configured duration."""
clip = ResolvedClip(
clip_id="c1",
asset_id="a1",
local_path=Path("/tmp/fake.mp4"),
clip_type="main",
order=0,
start_time=0.0,
duration=4.0,
actual_duration=0.0,
playback_speed=0.5,
)
assert UnifiedRenderService._clip_effective_duration(clip) == 4.0
class TestClipAdjustedDurationWithSpeedCompensation:
"""Test _clip_adjusted_duration accounts for compensated speed."""
def test_adjusted_duration_with_compensation(self):
"""Adjusted duration = min(duration, actual) / speed.
With compensation: min(4,3)/0.75 = 3/0.75 = 4.0
This equals the configured duration, which is the goal.
"""
clip = ResolvedClip(
clip_id="c1",
asset_id="a1",
local_path=Path("/tmp/fake.mp4"),
clip_type="main",
order=0,
start_time=0.0,
duration=4.0,
actual_duration=3.0,
playback_speed=0.75, # compensated
)
adjusted = UnifiedRenderService._clip_adjusted_duration(clip)
# min(4,3)/0.75 = 3/0.75 = 4.0 (matches configured duration)
assert abs(adjusted - 4.0) < 0.01
+45 -10
View File
@@ -38,6 +38,28 @@ def _segments(count: int, dur_min: float = 3.0, dur_max: float = 5.0):
return [(i, dur_min, dur_max) for i in range(count)]
def _patch_zero_noise():
"""消除 clips.py 排序随机噪声,用于确定性断言(如均衡分配)。
排序噪声(random.uniform(0, SCORE_RANDOM_NOISE_MAX))返回 0
其他 uniform 调用(片段时长随机)委托给独立 Random 实例,行为不变。
"""
import random as _stdlib_random
from app.api.routes.templates_editor import clips as clips_module
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX
_fallback = _stdlib_random.Random()
def _fake_uniform(a, b):
if b == SCORE_RANDOM_NOISE_MAX:
return 0.0
return _fallback.uniform(a, b)
return patch.object(clips_module.random, "uniform", _fake_uniform)
def _patch_segments(segments=None):
return patch(
"app.api.routes.templates_editor.clips._get_template_segments",
@@ -58,6 +80,10 @@ def _make_mock_asset(asset_id, duration):
asset = MagicMock()
asset.id = asset_id
asset.duration = duration
# score_asset 所需的属性(避免 MagicMock 导致类型比较错误)
asset.quality_score = None
asset.created_at = None
asset.metadata = {}
return asset
@@ -126,7 +152,8 @@ class TestEditorClipsBySegments:
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2"], required_clips_count=2)
with _patch_segments(DEFAULT_SEGMENTS):
# 均衡分配由 use_count 贪心保证,消除排序噪声后确定性断言
with _patch_zero_noise(), _patch_segments(DEFAULT_SEGMENTS):
result = create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
@@ -142,11 +169,12 @@ class TestEditorClipsBySegments:
clips_data = _get_clips_data_from_call(mock_plan_svc)
assert len(clips_data) == 4
# 验证轮询分配:a1, a2, a1, a2
assert clips_data[0]["asset_id"] == "a1"
assert clips_data[1]["asset_id"] == "a2"
assert clips_data[2]["asset_id"] == "a1"
assert clips_data[3]["asset_id"] == "a2"
# 验证均衡分配(贪心策略保证):2个素材分4个片段,每个素材恰好使用2次
from collections import Counter
asset_ids = [c["asset_id"] for c in clips_data]
counts = Counter(asset_ids)
assert counts["a1"] == 2 and counts["a2"] == 2
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_orders_start_at_zero(self, mock_storage):
@@ -413,8 +441,13 @@ class TestEditorClipsDurationAndStartTime:
assert mock_calc.call_count == 2
clips_data = _get_clips_data_from_call(mock_plan_svc)
assert clips_data[0]["start_time"] == 12.5
assert clips_data[1]["start_time"] == 18.0
# clips_data 按 order 排序,但分配顺序因 shuffle 而随机,
# 因此只验证两个 start_time 值都存在
start_times = {c["start_time"] for c in clips_data}
assert start_times == {12.5, 18.0}
# 验证 order 仍然有序
orders = [c["order"] for c in clips_data]
assert orders == sorted(orders)
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_asset_durations_deduped(self, mock_storage):
@@ -631,7 +664,8 @@ class TestReuseRatioGate:
clips_data = _get_clips_data_from_call(mock_plan_svc)
assert len(clips_data) == 13
# 1 个复用片段,占比 1/13 ≈ 7.7% ≤ 15%
assert reused.get("a1", 0.0) == 5.0
# 转场补偿: raw_duration = 5.0 + (13-1)*0.5/13 ≈ 5.462 → round(5.462,1) = 5.5
assert abs(reused.get("a1", 0.0) - 5.5) < 0.1
@patch("app.api.routes.templates_editor.clips.get_storage_service")
def test_reuse_ratio_exceeded_returns_400(self, mock_storage):
@@ -784,7 +818,8 @@ class TestClipsFromAssetsInvalidIds:
body = ClipsFromAssetsRequest(asset_ids=["a1", None, "", "a2"]) # type: ignore[list-item]
with _patch_segments(_segments(2, dur_min=3.0, dur_max=5.0)):
# 消除排序噪声,确定性断言两条合法素材各被使用
with _patch_zero_noise(), _patch_segments(_segments(2, dur_min=3.0, dur_max=5.0)):
result = create_clips_from_assets_editor(
template_id="tpl-001",
body=body,
+9 -7
View File
@@ -32,16 +32,16 @@ class TestRecommendedTimeConflicts:
assert _recommended_time_conflicts(10.0, 5.0, [(10.0, 15.0)]) is True
def test_touching_endpoint_conflicts_due_to_edge_gap(self):
"""首尾紧贴(推荐 15 开始,已用 [10,15]):0.3s 扩边内 → 冲突。"""
"""首尾紧贴(推荐 15 开始,已用 [10,15]):1.5s 扩边内 → 冲突。"""
assert _recommended_time_conflicts(15.0, 5.0, [(10.0, 15.0)]) is True
def test_gap_within_edge_gap_conflicts(self):
"""间隔 0.2s< 0.3s 边缘间隙)→ 冲突。"""
"""间隔 0.2s< 1.5s 边缘间隙)→ 冲突。"""
assert _recommended_time_conflicts(15.2, 5.0, [(10.0, 15.0)]) is True
def test_gap_beyond_edge_gap_no_conflict(self):
"""间隔 0.5s> 0.3s 边缘间隙)→ 不冲突。"""
assert _recommended_time_conflicts(15.5, 5.0, [(10.0, 15.0)]) is False
"""间隔 2.0s> 1.5s 边缘间隙)→ 不冲突。"""
assert _recommended_time_conflicts(17.0, 5.0, [(10.0, 15.0)]) is False
def test_far_apart_no_conflict(self):
"""相隔很远 → 不冲突。"""
@@ -54,7 +54,9 @@ class TestRecommendedTimeConflicts:
"""多个已用区间,任一冲突即返回 True。"""
used = [(0.0, 5.0), (10.0, 15.0), (20.0, 25.0)]
assert _recommended_time_conflicts(12.0, 2.0, used) is True
assert _recommended_time_conflicts(6.0, 2.0, used) is False
assert (
_recommended_time_conflicts(6.5, 2.0, used) is False
) # range [6.5,8.5], just outside all expanded used ranges
def test_custom_edge_gap(self):
"""edge_gap 可配置:gap=0 时紧贴不冲突(端点相接不算重叠)。"""
@@ -64,5 +66,5 @@ class TestRecommendedTimeConflicts:
assert _recommended_time_conflicts(15.5, 5.0, [(10.0, 15.0)], edge_gap=1.0) is True
def test_default_edge_gap_constant(self):
"""默认边缘间隙常量为 0.3s(配置常量)。"""
assert SEGMENT_EDGE_GAP == 0.3
"""默认边缘间隙常量为 1.5s(配置常量)。"""
assert SEGMENT_EDGE_GAP == 1.5
+56 -21
View File
@@ -69,13 +69,13 @@ class TestRecommendedTimeConflicts:
def test_conflict_exact_boundary_no_overlap(self):
from app.api.routes.templates_editor.clips import _recommended_time_conflicts
# 新语义:默认 0.3s 边缘间隙扩边,推荐 [10, 15] 与已用 [0, 10] 首尾相接
# 新语义:默认 1.5s 边缘间隙扩边,推荐 [10, 15] 与已用 [0, 10] 首尾相接
# 落在扩边范围内 → 判为冲突(避免观感重复)
assert _recommended_time_conflicts(10.0, 5.0, [(0.0, 10.0)]) is True
# 显式 edge_gap=0 时退回纯区间重叠判定:相接不算重叠
assert _recommended_time_conflicts(10.0, 5.0, [(0.0, 10.0)], edge_gap=0.0) is False
# 间隙大于边缘间隙(0.5 > 0.3)→ 不冲突
assert _recommended_time_conflicts(10.5, 5.0, [(0.0, 10.0)]) is False
# 间隙大于边缘间隙(2.0 > 1.5)→ 不冲突
assert _recommended_time_conflicts(12.0, 5.0, [(0.0, 10.0)]) is False
def test_conflict_multiple_used(self):
from app.api.routes.templates_editor.clips import _recommended_time_conflicts
@@ -83,10 +83,10 @@ class TestRecommendedTimeConflicts:
used = [(0.0, 5.0), (10.0, 15.0), (20.0, 25.0)]
# 推荐 [6, 11] 与 [10, 15] 冲突
assert _recommended_time_conflicts(6.0, 5.0, used) is True
# 推荐 [15, 20] 与 [10, 15] 首尾相接:0.3s 扩边内 → 冲突
# 推荐 [15, 20] 与 [10, 15] 首尾相接:1.5s 扩边内 → 冲突
assert _recommended_time_conflicts(15.0, 5.0, used) is True
# 空闲段 [5.3, 9.7] 长 4.4s:推荐 [5.5, 9.5]dur=4)与三区间扩边均不接触
assert _recommended_time_conflicts(5.5, 4.0, used) is False
# 空闲段 (6.5, 8.5) 长 2.0s:推荐 [6.6, 8.4]dur=1.8)与三区间扩边均不接触
assert _recommended_time_conflicts(6.6, 1.8, used) is False
# ── _get_mediakit_recommendations 单元测试 ──────────────────────────────────
@@ -306,6 +306,27 @@ class TestGetTemplateSegments:
# ── from-assets 端点集成测试 ────────────────────────────────────────────────
def _patch_zero_noise():
"""消除 clips.py 排序随机噪声(SCORE_RANDOM_NOISE_MAX 噪声返回 0)。
用于均衡分配等确定性断言;其他 uniform 调用(片段时长随机)行为不变。
"""
import random as _stdlib_random
from app.api.routes.templates_editor import clips as clips_module
from packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX
_fallback = _stdlib_random.Random()
def _fake_uniform(a, b):
if b == SCORE_RANDOM_NOISE_MAX:
return 0.0
return _fallback.uniform(a, b)
return patch.object(clips_module.random, "uniform", _fake_uniform)
def _make_auth_user():
auth = MagicMock()
auth.user.id = "user-001"
@@ -347,6 +368,10 @@ def _make_rich_asset(asset_id, duration, storage_key="v.mp4", mime="video/mp4"):
asset.duration = duration
asset.storage_key = storage_key
asset.mime_type = mime
# score_asset 所需的属性(避免 MagicMock 导致类型比较错误)
asset.quality_score = None
asset.created_at = None
asset.metadata = {}
return asset
@@ -442,11 +467,15 @@ class TestFromAssetsByTemplateSegments:
)
clips_data = _get_clips_data(mock_plan_svc)
assert 3.0 <= clips_data[0]["duration"] <= 5.0
assert 4.0 <= clips_data[1]["duration"] <= 8.0
# PR #1614 转场补偿:2 个片段时每 clip 时长 +(2-1)*0.5/2=0.25s
# (xfade 重叠在渲染时扣除,故 clip 时长 = segment 随机时长 + 补偿),
# 断言上界需计入补偿与一位小数舍入余量
comp = (2 - 1) * 0.5 / 2
assert 3.0 + comp - 0.1 <= clips_data[0]["duration"] <= 5.0 + comp + 0.1
assert 4.0 + comp - 0.1 <= clips_data[1]["duration"] <= 8.0 + comp + 0.1
def test_assets_round_robin_assignment(self):
"""素材按片段顺序轮询分配"""
def test_assets_balanced_assignment(self):
"""素材按使用次数贪心分配(使用少的优先),保证均衡使用"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
@@ -462,20 +491,26 @@ class TestFromAssetsByTemplateSegments:
mock_asset_repo.get.side_effect = get_asset
body = ClipsFromAssetsRequest(asset_ids=["a1", "a2"])
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="plan-1",
services=(mock_tpl_svc, mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
# 消除排序噪声,确定性断言贪心均衡分配
with _patch_zero_noise():
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="plan-1",
services=(mock_tpl_svc, mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
clips_data = _get_clips_data(mock_plan_svc)
asset_ids = [c["asset_id"] for c in clips_data]
assert asset_ids == ["a1", "a2", "a1", "a2"]
# 贪心分配保证均衡:2个素材分4个片段,每个素材恰好使用2次
from collections import Counter
counts = Counter(asset_ids)
assert counts["a1"] == 2 and counts["a2"] == 2
def test_orders_start_from_zero(self):
"""片段 order 从 0 开始递增。"""
+4
View File
@@ -908,6 +908,10 @@ class TestAssetDurationsAlwaysFetched:
def fake_get(asset_id):
mock_asset = MagicMock()
mock_asset.duration = 30.0 # 每个素材 30 秒
# score_asset 所需的属性
mock_asset.quality_score = None
mock_asset.created_at = None
mock_asset.metadata = {}
return mock_asset
asset_repo.get = MagicMock(side_effect=fake_get)
+257
View File
@@ -0,0 +1,257 @@
"""Tests for scene-change smart frame selection + random shuffle of segment processing."""
from __future__ import annotations
import random
from unittest.mock import MagicMock, patch
import pytest
from apps.api.app.api.routes.templates_editor.clips import (
_build_scene_segments,
_pick_start_in_scene_segment,
)
from packages.shared.mediakit_client import MediaKitClient
# ── Part 1: Random shuffle tests ──────────────────────────────────────────
class TestRandomShuffle:
"""验证 segments 处理顺序随机打乱逻辑."""
def test_same_segments_produce_different_asset_orders(self):
"""同一批 segments 多次处理,asset 分配顺序有变化.
模拟打乱后的处理顺序,验证多次运行中 asset_id 分配顺序
存在差异(概率性验证,运行 50 次应该至少出现 2 种排列)。
"""
segments = [(0, 3.0, 5.0), (1, 4.0, 6.0), (2, 3.0, 5.0), (3, 4.0, 6.0)]
asset_ids = ["A", "B", "C", "D"]
observed_orders: list[tuple] = set()
for _ in range(50):
shuffled_indices = list(range(len(segments)))
random.shuffle(shuffled_indices)
order_tuple = tuple(shuffled_indices)
observed_orders.add(order_tuple)
# 50 次打乱,4! = 24 种排列,应出现多种不同排列
assert len(observed_orders) > 1, "打乱应该产生多种不同顺序"
def test_clips_data_order_always_sorted(self):
"""clips_data 按 order 排序后始终有序.
模拟打乱处理后 clips_data 按 order 排序,验证最终 order 为 [0,1,2,3]。
"""
segments = [(0, 3.0, 5.0), (1, 4.0, 6.0), (2, 3.0, 5.0), (3, 4.0, 6.0)]
for _ in range(20):
shuffled_indices = list(range(len(segments)))
random.shuffle(shuffled_indices)
# 模拟构建 clips_data(用 _seg_order 作为 order
clips_data = []
for idx in shuffled_indices:
seg_order, _, _ = segments[idx]
clips_data.append({"order": seg_order, "asset_id": f"asset_{idx}"})
# 按 order 排序
clips_data.sort(key=lambda c: c["order"])
# 验证 order 始终有序
orders = [c["order"] for c in clips_data]
assert orders == [0, 1, 2, 3], f"排序后 order 应为 [0,1,2,3],实际为 {orders}"
# ── Part 2: detect_scene_changes tests ────────────────────────────────────
class TestDetectSceneChanges:
"""验证 MediaKitClient.detect_scene_changes 方法."""
def _make_client(self) -> MediaKitClient:
"""创建一个可用的 MediaKitClientmock 配置)."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = "test-key"
mock_settings.return_value.mediakit_base_url = "http://test"
mock_settings.return_value.mediakit_timeout = 30
client = MediaKitClient()
return client
def test_scene_change_success(self):
"""SceneChange 策略成功返回时间戳列表."""
client = self._make_client()
mock_frames = [
{"image_url": "url1", "timestamp": 0.0},
{"image_url": "url2", "timestamp": 3.2},
{"image_url": "url3", "timestamp": 7.8},
{"image_url": "url4", "timestamp": 12.5},
]
with patch.object(client, "extract_frames", return_value=mock_frames):
result = client.detect_scene_changes("https://example.com/video.mp4")
assert result is not None
assert result[0] == 0.0 # 始终以 0.0 开头
assert 3.2 in result
assert 7.8 in result
assert 12.5 in result
assert result == sorted(result) # 应已排序
def test_scene_change_fallback_to_time_interval(self):
"""SceneChange 失败降级到 TimeInterval 策略."""
client = self._make_client()
# 第一次调用(SceneChange)返回 None,第二次(TimeInterval)返回结果
fallback_frames = [
{"image_url": "url1", "timestamp": 0.0},
{"image_url": "url2", "timestamp": 5.0},
{"image_url": "url3", "timestamp": 10.0},
]
call_count = 0
def side_effect(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count == 1:
# 第一次 SceneChange 失败
return None
else:
# 第二次 TimeInterval 成功
assert kwargs.get("strategy") == "TimeInterval"
return fallback_frames
with patch.object(client, "extract_frames", side_effect=side_effect):
result = client.detect_scene_changes("https://example.com/video.mp4")
assert result is not None
assert result[0] == 0.0
assert 5.0 in result
assert 10.0 in result
def test_mediakit_not_available_returns_none(self):
"""MediaKit 不可用时返回 None."""
with patch("packages.shared.mediakit_client.get_shared_settings") as mock_settings:
mock_settings.return_value.mediakit_api_key = "" # 未配置
mock_settings.return_value.mediakit_base_url = "http://test"
mock_settings.return_value.mediakit_timeout = 30
client = MediaKitClient()
result = client.detect_scene_changes("https://example.com/video.mp4")
assert result is None
def test_both_strategies_fail_returns_none(self):
"""SceneChange 和 TimeInterval 都失败时返回 None."""
client = self._make_client()
with patch.object(client, "extract_frames", return_value=None):
result = client.detect_scene_changes("https://example.com/video.mp4")
assert result is None
def test_prepends_zero_if_not_present(self):
"""若帧列表中不包含 0.0,自动在开头添加."""
client = self._make_client()
# 帧列表中没有 timestamp=0.0
mock_frames = [
{"image_url": "url1", "timestamp": 2.0},
{"image_url": "url2", "timestamp": 5.5},
]
with patch.object(client, "extract_frames", return_value=mock_frames):
result = client.detect_scene_changes("https://example.com/video.mp4")
assert result is not None
assert result[0] == 0.0
assert 2.0 in result
assert 5.5 in result
# ── Part 2.2: Scene segment building and assignment ───────────────────────
class TestSceneSegments:
"""验证镜头段构建和分配逻辑."""
def test_build_scene_segments(self):
"""从场景切换点正确构建镜头段."""
scene_changes = [0.0, 3.2, 7.8, 12.5]
asset_duration = 15.0
segments = _build_scene_segments(scene_changes, asset_duration)
assert len(segments) == 4
assert segments[0] == (0.0, 3.2)
assert segments[1] == (3.2, 7.8)
assert segments[2] == (7.8, 12.5)
assert segments[3] == (12.5, 15.0)
def test_build_scene_segments_filters_short(self):
"""过滤掉过短的镜头段(< 0.5秒)."""
scene_changes = [0.0, 0.1, 5.0, 5.3, 10.0]
asset_duration = 12.0
segments = _build_scene_segments(scene_changes, asset_duration)
# (0.0, 0.1) 长度 0.1 < 0.5 → 过滤
# (0.1, 5.0) → 保留
# (5.0, 5.3) 长度 0.3 < 0.5 → 过滤
# (5.3, 10.0) → 保留
# (10.0, 12.0) → 保留
assert len(segments) == 3
assert segments[0] == (0.1, 5.0)
assert segments[1] == (5.3, 10.0)
assert segments[2] == (10.0, 12.0)
def test_pick_start_in_segment(self):
"""在镜头段内随机选取起始时间."""
seg_start = 3.0
seg_end = 8.0
clip_duration = 2.0
starts = set()
for _ in range(100):
start = _pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
assert start is not None
assert seg_start <= start <= seg_end - clip_duration
starts.add(round(start, 2))
# 应该有多个不同的起始时间
assert len(starts) > 1
def test_pick_start_segment_too_short(self):
"""镜头段太短无法容纳片段时返回 None."""
result = _pick_start_in_scene_segment(0.0, 1.0, 2.0)
assert result is None
def test_different_clips_from_different_scenes(self):
"""不同片段应来自不同的镜头段(模拟分配逻辑)."""
scene_changes = [0.0, 5.0, 10.0, 15.0]
asset_duration = 18.0
clip_duration = 3.0
segments = _build_scene_segments(scene_changes, asset_duration)
assert len(segments) == 4 # (0,5), (5,10), (10,15), (15,18)
# 模拟 3 个片段从不同镜头段取点
scene_pool = list(segments)
assigned_starts = []
for _ in range(3):
if not scene_pool:
break
seg_start, seg_end = scene_pool.pop(0)
start = _pick_start_in_scene_segment(seg_start, seg_end, clip_duration)
assert start is not None
assigned_starts.append(start)
# 3 个片段分别从 3 个不同镜头段中选取
assert len(assigned_starts) == 3
# 第一个来自 [0, 2],第二个来自 [5, 7],第三个来自 [10, 12]
assert 0.0 <= assigned_starts[0] <= 2.0
assert 5.0 <= assigned_starts[1] <= 7.0
assert 10.0 <= assigned_starts[2] <= 12.0
+475
View File
@@ -0,0 +1,475 @@
"""测试 smart_match 评分集成到素材选取路径。
验证:
- 使用次数多的素材评分低于使用次数少的(unused 维度降权生效)
- from-assets 路径中 sorted_candidates 按 smart_match 评分排序
- 一键生成路径中 _sort_assets_by_smart_score 按评分降序
"""
from __future__ import annotations
import os
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
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 packages.domain.smart_match import SCORE_RANDOM_NOISE_MAX, score_asset, smart_select_assets
# ── 辅助工厂 ──────────────────────────────────────────────────────────────────
@dataclass
class FakeAsset:
"""Minimal Asset-like object."""
id: str
quality_score: float | None = 70.0
duration: float = 15.0
created_at: datetime | None = None
metadata: dict[str, Any] = field(default_factory=dict)
status: str = "ready"
file_type: str = "video"
def _asset_with_use_count(asset_id: str, use_count: int) -> FakeAsset:
"""创建指定使用次数的素材,其他维度保持一致。"""
return FakeAsset(
id=asset_id,
quality_score=70.0,
duration=15.0, # 最优区间 5-30s
created_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
metadata={"generation_use_count": use_count},
)
# ── score_asset 单元测试:unused 维度降权 ─────────────────────────────────────
class TestScoreAssetUnusedDiminsh:
"""验证 unused 维度:使用次数越多,评分越低。"""
def test_unused_scores_higher_than_used(self):
"""use_count=0 的素材评分高于 use_count>0 的。"""
fresh = _asset_with_use_count("fresh", 0)
used = _asset_with_use_count("used", 1)
fresh_score, _ = score_asset(fresh)
used_score, _ = score_asset(used)
assert fresh_score > used_score
def test_high_use_count_scores_lower_than_low(self):
"""use_count=5 的素材评分低于 use_count=1 的。"""
low_use = _asset_with_use_count("low", 1)
high_use = _asset_with_use_count("high", 5)
low_score, _ = score_asset(low_use)
high_score, _ = score_asset(high_use)
assert low_score > high_score
def test_unused_breakdown_values(self):
"""验证 unused 维度的具体分值。"""
fresh = _asset_with_use_count("fresh", 0)
low = _asset_with_use_count("low", 2)
high = _asset_with_use_count("high", 10)
_, fresh_bd = score_asset(fresh)
_, low_bd = score_asset(low)
_, high_bd = score_asset(high)
# use_count=0 → unused_score=100 → component=10.0
assert fresh_bd["unused"] == 10.0
# use_count=2 → unused_score=70 → component=7.0
assert low_bd["unused"] == 7.0
# use_count=10 → unused_score=30 → component=3.0
assert high_bd["unused"] == 3.0
def test_monotonically_decreasing_scores(self):
"""使用次数递增时,总评分单调不增。"""
scores = []
for count in [0, 1, 2, 3, 5, 10, 50]:
a = _asset_with_use_count(f"a{count}", count)
s, _ = score_asset(a)
scores.append(s)
# 验证非递增
for i in range(len(scores) - 1):
assert (
scores[i] >= scores[i + 1]
), f"use_count 递增时评分应不增: scores[{i}]={scores[i]} < scores[{i+1}]={scores[i+1]}"
# ── smart_select_assets 排序测试 ─────────────────────────────────────────────
class TestSmartSelectAssetsOrdering:
"""验证 smart_select_assets 返回结果按评分降序。"""
def test_less_used_assets_ranked_higher(self):
"""使用次数少的素材在结果中排名更高。"""
assets = [
_asset_with_use_count("heavily_used", 10),
_asset_with_use_count("never_used", 0),
_asset_with_use_count("lightly_used", 2),
]
results = smart_select_assets(assets)
ids = [r.asset.id for r in results]
# never_used 排第一,heavily_used 排最后
assert ids[0] == "never_used"
assert ids[-1] == "heavily_used"
def test_same_quality_different_use_count(self):
"""质量相同时,使用次数少的排名更高。"""
assets = [
_asset_with_use_count("used_5", 5),
_asset_with_use_count("used_0", 0),
]
results = smart_select_assets(assets)
assert results[0].asset.id == "used_0"
assert results[1].asset.id == "used_5"
# ── from-assets 路径集成测试 ─────────────────────────────────────────────────
def _make_mock_asset_for_clips(aid, duration, use_count=0):
"""创建带 score_asset 所需属性的 mock 素材。"""
asset = MagicMock()
asset.id = aid
asset.duration = duration
asset.quality_score = None
asset.created_at = None
asset.metadata = {"generation_use_count": use_count}
return asset
def _make_auth_user():
auth = MagicMock()
auth.user.id = "user-001"
auth.user.email = "test@example.com"
auth.user.display_name = "测试用户"
auth.user_id = "user-001"
return auth
def _make_zero_noise_patcher(module):
"""构造 patch(module.random.uniform):噪声调用(上界=SCORE_RANDOM_NOISE_MAX)返回 0。
其他 uniform 调用(如片段时长随机)委托给一个独立的 Random 实例,
避免递归回已 patch 的全局函数。
"""
import random as _stdlib_random
_fallback = _stdlib_random.Random()
def _fake_uniform(a, b):
if b == SCORE_RANDOM_NOISE_MAX:
return 0.0
return _fallback.uniform(a, b)
return patch.object(module.random, "uniform", _fake_uniform)
def _patch_zero_noise_clips():
"""消除 clips.py 排序噪声,其他 uniform 调用不受影响。"""
from app.api.routes.templates_editor import clips as clips_module
return _make_zero_noise_patcher(clips_module)
def _patch_zero_noise_plan_service():
"""消除 plan_generator_service.py 排序噪声,其他 uniform 调用不受影响。"""
from app.services import plan_generator_service as svc_module
return _make_zero_noise_patcher(svc_module)
class TestFromAssetsSmartMatchIntegration:
"""验证 clips.py 中 sorted_candidates 使用 smart_match 评分。"""
def test_sorted_candidates_prefers_high_score_low_use_count(self):
"""在 from-assets 路径中,smart_match 分高且使用次数少的素材排在前面。"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
mock_asset_repo = MagicMock()
def _get_asset(aid):
use_count = {"a_heavy": 10, "a_fresh": 0}[aid]
return _make_mock_asset_for_clips(aid, 30.0, use_count)
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
mock_plan_svc = MagicMock()
mock_plan_svc.replace_all_clips_transactional = MagicMock(return_value=2)
segments = [(0, 3.0, 5.0), (1, 3.0, 5.0)]
with (
_patch_zero_noise_clips(),
patch(
"app.api.routes.templates_editor.clips._get_template_segments",
return_value=segments,
),
patch(
"app.api.routes.templates_editor.clips.get_used_segments",
return_value={},
),
patch(
"app.api.routes.templates_editor.clips.record_used_segments",
return_value=None,
),
):
body = ClipsFromAssetsRequest(
asset_ids=["a_heavy", "a_fresh"],
required_clips_count=2,
)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id="test-plan-001",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
# 验证 replace_all_clips_transactional 被调用
assert mock_plan_svc.replace_all_clips_transactional.called
call_args = mock_plan_svc.replace_all_clips_transactional.call_args
clips_data = call_args.args[1]
# 第一个片段应该分配给 a_freshsmart_match 分更高)
first_clip_asset = clips_data[0]["asset_id"]
assert (
first_clip_asset == "a_fresh"
), f"第一个片段应分配给 smart_match 分更高的 a_fresh,实际是 {first_clip_asset}"
def test_score_noise_causes_varied_selection(self):
"""得分接近(差距 < SCORE_RANDOM_NOISE_MAX)的素材,多次生成的素材组合应有变化。
两条同等质量/时长/新鲜度的素材(use_count 相同),smart_match 得分一致,
噪声让两者的相对排名随机浮动,多次调用首个片段的素材分布应两者都出现。
"""
from app.api.routes.templates_editor.clips import create_clips_from_assets_editor
from app.api.routes.templates_editor.schemas import ClipsFromAssetsRequest
def _get_asset(aid):
return _make_mock_asset_for_clips(aid, 30.0, 0)
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
segments = [(0, 3.0, 5.0), (1, 3.0, 5.0)]
first_assets: set[str] = set()
for _ in range(30):
mock_plan_svc = MagicMock()
mock_plan_svc.replace_all_clips_transactional = MagicMock(return_value=2)
with (
patch(
"app.api.routes.templates_editor.clips._get_template_segments",
return_value=segments,
),
patch(
"app.api.routes.templates_editor.clips.get_used_segments",
return_value={},
),
patch(
"app.api.routes.templates_editor.clips.record_used_segments",
return_value=None,
),
):
body = ClipsFromAssetsRequest(
asset_ids=["a_x", "a_y"],
required_clips_count=2,
)
create_clips_from_assets_editor(
template_id="tmpl-1",
body=body,
background_tasks=MagicMock(),
plan_id=f"plan-noise-{len(first_assets)}-{_}",
services=(MagicMock(), mock_plan_svc),
asset_repo=mock_asset_repo,
db=MagicMock(),
current_user=_make_auth_user(),
)
clips_data = mock_plan_svc.replace_all_clips_transactional.call_args.args[1]
first_assets.add(clips_data[0]["asset_id"])
assert first_assets == {
"a_x",
"a_y",
}, f"噪声应使两条等分素材的排名浮动,30 次调用首个片段应覆盖两者,实际 {first_assets}"
# ── 一键生成路径集成测试 ─────────────────────────────────────────────────────
class TestPlanGeneratorSmartMatchIntegration:
"""验证 PlanGeneratorService._sort_assets_by_smart_score 排序正确。"""
def test_sort_assets_by_smart_score_descending(self):
"""_sort_assets_by_smart_score 返回按评分降序排列的素材 ID。"""
from app.services.plan_generator_service import PlanGeneratorService
mock_asset_repo = MagicMock()
def _get_asset(aid):
use_count = {"high_use": 10, "low_use": 0, "mid_use": 3}[aid]
asset = MagicMock()
asset.id = aid
asset.duration = 15.0
asset.quality_score = None
asset.created_at = None
asset.metadata = {"generation_use_count": use_count}
return asset
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
db = MagicMock()
svc = PlanGeneratorService(db, asset_repo=mock_asset_repo)
with _patch_zero_noise_plan_service():
sorted_ids = svc._sort_assets_by_smart_score(["high_use", "low_use", "mid_use"])
# low_use (0次) 应排第一,high_use (10次) 应排最后
assert sorted_ids[0] == "low_use"
assert sorted_ids[-1] == "high_use"
assert sorted_ids[1] == "mid_use"
def test_distribute_assets_uses_smart_score_ordering(self):
"""_distribute_assets 在非随机模式下按 smart_match 评分排序素材。"""
from app.services.plan_generator_service import PlanGeneratorService
from packages.domain.edit_plan_clip import EditPlanClip
from packages.domain.editing_mode import EditingMode
mock_asset_repo = MagicMock()
def _get_asset(aid):
use_count = {"old_asset": 10, "new_asset": 0}[aid]
asset = MagicMock()
asset.id = aid
asset.duration = 30.0
asset.quality_score = None
asset.created_at = None
asset.metadata = {"generation_use_count": use_count}
return asset
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
db = MagicMock()
svc = PlanGeneratorService(db, asset_repo=mock_asset_repo)
# 创建 2 个 main clips(需要提供 id 参数)
clips = [
EditPlanClip(id="c1", plan_id="p1", clip_type="main", duration=5.0, order=0),
EditPlanClip(id="c2", plan_id="p1", clip_type="main", duration=5.0, order=1),
]
with (
_patch_zero_noise_plan_service(),
patch("app.services.plan_generator_service.distribute_assets") as mock_dist,
):
svc._distribute_assets(
clips,
["old_asset", "new_asset"],
EditingMode.ONE_TAKE.value,
random_selection=False,
)
# 验证传给 distribute_assets 的 asset_ids 按 smart_match 排序
call_args = mock_dist.call_args
passed_ids = call_args.args[1]
# new_asset (0次使用) 应排在 old_asset (10次使用) 前面
assert passed_ids[0] == "new_asset"
assert passed_ids[1] == "old_asset"
def test_random_selection_skips_smart_score_sort(self):
"""random_selection=True 时不执行 smart_match 排序。"""
from app.services.plan_generator_service import PlanGeneratorService
from packages.domain.edit_plan_clip import EditPlanClip
from packages.domain.editing_mode import EditingMode
mock_asset_repo = MagicMock()
db = MagicMock()
svc = PlanGeneratorService(db, asset_repo=mock_asset_repo)
clips = [
EditPlanClip(id="c1", plan_id="p1", clip_type="main", duration=5.0, order=0),
]
with patch("app.services.plan_generator_service.distribute_assets") as mock_dist:
svc._distribute_assets(
clips,
["a1", "a2"],
EditingMode.ONE_TAKE.value,
random_selection=True,
)
# random_selection=True 时不应调用 asset_repo.get(不执行排序)
mock_asset_repo.get.assert_not_called()
class TestPlanGeneratorScoreNoise:
"""验证一键生成路径的评分排序注入了随机噪声。"""
def test_equal_scores_produce_varied_order(self):
"""两条 smart_match 得分相同的素材,多次排序的首位应覆盖两者。"""
from app.services.plan_generator_service import PlanGeneratorService
def _get_asset(aid):
asset = MagicMock()
asset.id = aid
asset.duration = 15.0
asset.quality_score = None
asset.created_at = None
asset.metadata = {"generation_use_count": 0}
return asset
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
svc = PlanGeneratorService(MagicMock(), asset_repo=mock_asset_repo)
first_ids: set[str] = set()
for _ in range(30):
order = svc._sort_assets_by_smart_score(["equal_a", "equal_b"])
first_ids.add(order[0])
assert first_ids == {
"equal_a",
"equal_b",
}, f"噪声应使等分素材排名浮动,30 次排序首位应覆盖两者,实际 {first_ids}"
def test_large_score_gap_not_flipped(self):
"""得分差距远大于噪声上限时,低分素材不会因噪声超过高分素材。
quality 100 vs 0 → quality 维度差距 40 分 > 噪声上限 20,
其余维度完全一致,50 次排序高质量素材必须始终排第一。
"""
from app.services.plan_generator_service import PlanGeneratorService
def _get_asset(aid):
quality = {"top": 100.0, "bad": 0.0}[aid]
asset = MagicMock()
asset.id = aid
asset.duration = 15.0
asset.quality_score = quality
asset.created_at = None
asset.metadata = {"generation_use_count": 0}
return asset
mock_asset_repo = MagicMock()
mock_asset_repo.get = MagicMock(side_effect=_get_asset)
svc = PlanGeneratorService(MagicMock(), asset_repo=mock_asset_repo)
for _ in range(50):
order = svc._sort_assets_by_smart_score(["top", "bad"])
assert order[0] == "top", f"质量差距 40 分 > 噪声上限,top 应始终排第一,实际 {order}"