Files
xiaoxia-saas/apps/worker/video_processing/dedup_helpers.py
T
saas-backend-agent aee29a45a4
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 26s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m53s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m52s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m5s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 290h45m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 290h45m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 290h45m41s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 290h45m49s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 290h45m53s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 290h46m19s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 290h46m29s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 290h45m47s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 290h46m31s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 290h46m29s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 291h20m1s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 291h20m14s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 291h20m44s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 291h20m55s
fix: 查重流程全面核实修复 — 两阶段持久化 + 重新计算查重API (#1664)
问题根因:
1. dedup_helpers.py 先 commit 视频记录再算查重率 — 如果指纹计算
   或查重率计算中途异常,视频已入库但 duplicate_rate=None,且无重试
2. 没有 API 可以触发已有视频重新计算查重率
3. 前端 visual_similarity 显示 bug(后端返 0~1,前端直接 toFixed%)

修复:
- dedup_helpers.py 重构为两阶段持久化:先计算全部指纹/查重数据
  (内存),再一次性 create + commit,消除中间态
- videos.py 新增 POST /videos/recompute-dedup 端点:对缺少查重数据
  的视频触发异步 check_duplicate 任务重新计算
- 更新 test_dedup_helpers_user_id 适配新持久化模式

新增测试:
- test_dedup_two_phase_commit.py: 5 个测试覆盖两阶段提交场景
- test_recompute_dedup_api.py: 5 个测试覆盖重新计算 API
2026-09-04 12:37:52 +08:00

171 lines
6.3 KiB
Python
Executable File

"""查重辅助函数 — 从 generation.py 提取的 GeneratedVideo 记录 + 查重逻辑.
供 generate_video 共同复用,
创建 GeneratedVideo 记录后计算指纹并执行项目级 + 批次内查重。
v2: 两阶段持久化 — 先计算所有查重数据,再一次性 commit,
避免中间异常导致 duplicate_rate 等字段缺失。
"""
from __future__ import annotations
import logging
from uuid import uuid4
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
def create_video_record_and_dedup(
*,
generation_task_id: str,
project_id: str,
user_id: str = "",
batch_id: str,
file_url: str,
file_size: int,
duration: float,
video_path: str,
mode: str,
session: Session,
width: int = 1280,
height: int = 720,
fps: float = 25.0,
name: str = "",
thumbnail_url: str = "",
) -> int:
"""创建 GeneratedVideo 记录,计算指纹并执行查重(历史 + 批次)。
采用两阶段持久化:先计算所有指纹/查重数据(内存),
再一次性写入数据库并 commit。若指纹计算失败,
视频记录仍会创建(无查重数据),但保证不会出现"写了记录却没 commit"的中间态。
Returns:
创建的视频记录数量(1 表示成功,0 表示失败)
"""
from video_processing.dedup import VideoDeduplicator, _save_fingerprint_chunks
from packages.adapters.sqlalchemy_impl.generated_video_repository import (
SQLAlchemyGeneratedVideoRepository,
)
from packages.domain import GeneratedVideo
try:
video_id = uuid4().hex
video_name = name.strip() if name else f"generated-{generation_task_id[:8]}.mp4"
# ── Phase 1: 构建视频记录(内存,不 commit) ────────────────
generated_video = GeneratedVideo(
id=video_id,
project_id=project_id,
user_id=user_id,
generation_task_id=generation_task_id,
name=video_name,
file_url=file_url,
file_size=file_size,
duration=duration,
width=width,
height=height,
fps=fps,
status="completed",
generation_params={"mode": mode},
thumbnail_url=thumbnail_url or None,
)
# ── Phase 2: 计算指纹 & 查重(全部在内存) ────────────────
deduplicator = VideoDeduplicator()
fingerprint = None
try:
fingerprint = deduplicator.compute_fingerprint(video_path)
except Exception as fp_err:
logger.warning("Fingerprint computation failed for %s: %s", video_id, fp_err)
if fingerprint is not None:
generated_video.video_fingerprint = fingerprint.to_dict()
# 写入分片指纹表(失败不阻塞)
try:
_save_fingerprint_chunks(fingerprint, video_id, project_id, user_id, session)
except Exception as chunk_err:
logger.warning("Failed to save fingerprint chunks for %s: %s", video_id, chunk_err)
# (a) 历史成片查重(跨项目全局 + 时长预过滤)
duration_sec = fingerprint.duration / 1000 if fingerprint.duration else 0
duplicate_result = deduplicator.check_duplicate(
fingerprint,
project_id,
session,
scope="user",
user_id=user_id,
duration_sec=duration_sec,
)
# (b) 批次内查重(仅当有 batch_id 时)
if not duplicate_result and batch_id:
duplicate_result = deduplicator.check_batch_duplicate(fingerprint, batch_id, video_id, session)
if duplicate_result:
generated_video.is_duplicate = True
generated_video.duplicate_of = duplicate_result["duplicate_of"]
logger.info(
"Duplicate detected: %s -> %s (reason=%s, similarity=%.3f)",
video_id,
duplicate_result["duplicate_of"],
duplicate_result["reason"],
duplicate_result["similarity"],
)
else:
generated_video.is_duplicate = False
generated_video.duplicate_of = None
# 计算重复率百分比(跨项目全局)
try:
rate_result = deduplicator.compute_duplicate_rate(
fingerprint,
project_id,
video_id,
session,
scope="user",
user_id=user_id,
)
generated_video.duplicate_rate = rate_result["duplicate_rate"]
generated_video.match_count = rate_result["match_count"]
generated_video.visual_similarity = rate_result["visual_similarity"]
logger.info(
"Duplicate rate for %s: %.2f%% (visual_sim=%.3f, matches=%d)",
video_id,
rate_result["duplicate_rate"],
rate_result["visual_similarity"],
rate_result["match_count"],
)
except Exception as rate_err:
logger.warning("Failed to compute duplicate_rate for %s: %s", video_id, rate_err)
generated_video.duplicate_rate = None
# ── Phase 3: 一次性持久化 ─────────────────────────────────
video_repo = SQLAlchemyGeneratedVideoRepository(session)
video_repo.create(generated_video)
if thumbnail_url:
logger.info("Thumbnail set for video %s: %s", video_id, thumbnail_url[:80])
session.commit()
logger.info(
"GeneratedVideo record created: %s (task=%s, dup=%s, rate=%s)",
video_id,
generation_task_id,
generated_video.is_duplicate,
generated_video.duplicate_rate,
)
return 1
except Exception as e:
logger.error(
"Failed to create video record / dedup for task %s: %s",
generation_task_id,
e,
)
session.rollback()
return 0