15909a92e5
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 (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 28s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 28s
CI/CD Pipeline / Build Staging API Image (push) Successful in 22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 50s
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 / 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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (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 / Validate - Python (mypy + alembic) (push) Successful in 2m11s
AI Code Review / AI Code Review (pull_request) Successful in 1m58s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m45s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m45s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m18s
CI/CD Pipeline / Validate - Style (push) Successful in 2m57s
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 / CI Gate (pull_request) Successful in 3s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m38s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 50s
CI/CD Pipeline / Validate - Security (push) Successful in 4m29s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m29s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m1s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m2s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m3s
CI/CD Pipeline / Unit Tests (push) Successful in 10m33s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
173 lines
6.4 KiB
Python
Executable File
173 lines
6.4 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) 历史成片查重(跨项目全局 + 时长预过滤)
|
|
# Issue #1702: fingerprint.duration 单位是秒,旧代码 /1000 让时长预过滤失效
|
|
duration_sec = fingerprint.duration if fingerprint.duration else 0
|
|
duplicate_result = deduplicator.check_duplicate(
|
|
fingerprint,
|
|
project_id,
|
|
session,
|
|
scope="user",
|
|
user_id=user_id,
|
|
duration_sec=duration_sec,
|
|
exclude_video_id=video_id,
|
|
)
|
|
|
|
# (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
|