2205adb8fb
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 3s
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 / PR Build Worker Image (push) 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 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) 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 - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 / Build Staging Web Image (push) Successful in 29s
CI/CD Pipeline / Build Staging API Image (push) Successful in 31s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 39s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
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 / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m49s
CI/CD Pipeline / CI Gate (pull_request) Successful in 11s
CI/CD Pipeline / Integration Tests (push) Successful in 2m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m25s
CI/CD Pipeline / Validate - Style (push) Successful in 2m46s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m11s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m58s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m50s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m33s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m13s
CI/CD Pipeline / Validate - Security (push) Successful in 5m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 5m9s
CI/CD Pipeline / Unit Tests (push) Successful in 8m9s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
155 lines
4.8 KiB
Python
155 lines
4.8 KiB
Python
"""查重记录领域实体。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class DuplicateSegment:
|
|
"""重复片段 — 描述上传视频中的一段与已有视频的匹配关系。"""
|
|
|
|
id: str
|
|
source_start: float
|
|
source_end: float
|
|
matched_video_id: str
|
|
matched_video_name: str
|
|
matched_start: float
|
|
matched_end: float
|
|
similarity: float # 0-100
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
source_start: float,
|
|
source_end: float,
|
|
matched_video_id: str,
|
|
matched_video_name: str,
|
|
matched_start: float,
|
|
matched_end: float,
|
|
similarity: float,
|
|
) -> "DuplicateSegment":
|
|
if source_start < 0 or source_end <= source_start:
|
|
raise ValueError("invalid source segment range")
|
|
if matched_start < 0 or matched_end <= matched_start:
|
|
raise ValueError("invalid matched segment range")
|
|
if not 0 <= similarity <= 100:
|
|
raise ValueError("similarity must be between 0 and 100")
|
|
return cls(
|
|
id=uuid4().hex,
|
|
source_start=source_start,
|
|
source_end=source_end,
|
|
matched_video_id=matched_video_id,
|
|
matched_video_name=matched_video_name,
|
|
matched_start=matched_start,
|
|
matched_end=matched_end,
|
|
similarity=similarity,
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class DuplicationRecord:
|
|
"""查重记录 — 一次视频查重请求的完整生命周期。"""
|
|
|
|
id: str
|
|
user_id: str
|
|
filename: str
|
|
file_size: int
|
|
storage_key: str # OSS 对象键
|
|
duration_seconds: float = 0.0
|
|
status: str = "pending" # pending / processing / completed / failed
|
|
duplicate_rate: float | None = None # 0-100
|
|
duplicate_count: int = 0
|
|
# #1661 手动查重:视觉相似度(归一化 0~1)/ 匹配视频数
|
|
visual_similarity: float | None = None
|
|
match_count: int | None = None
|
|
video_fingerprint: dict[str, Any] | None = None
|
|
error_message: str = ""
|
|
segments: list[DuplicateSegment] = field(default_factory=list)
|
|
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
user_id: str,
|
|
filename: str,
|
|
file_size: int,
|
|
storage_key: str,
|
|
*,
|
|
duration_seconds: float = 0.0,
|
|
) -> "DuplicationRecord":
|
|
if not user_id.strip():
|
|
raise ValueError("user_id cannot be empty")
|
|
if not filename.strip():
|
|
raise ValueError("filename cannot be empty")
|
|
if file_size <= 0:
|
|
raise ValueError("file_size must be positive")
|
|
return cls(
|
|
id=uuid4().hex,
|
|
user_id=user_id.strip(),
|
|
filename=filename.strip(),
|
|
file_size=file_size,
|
|
storage_key=storage_key,
|
|
duration_seconds=duration_seconds,
|
|
)
|
|
|
|
def mark_processing(self) -> None:
|
|
self.status = "processing"
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
def mark_completed(
|
|
self,
|
|
duplicate_rate: float,
|
|
duplicate_count: int,
|
|
segments: list[DuplicateSegment],
|
|
*,
|
|
visual_similarity: float | None = None,
|
|
match_count: int | None = None,
|
|
) -> None:
|
|
if not 0 <= duplicate_rate <= 100:
|
|
raise ValueError("duplicate_rate must be between 0 and 100")
|
|
self.status = "completed"
|
|
self.duplicate_rate = duplicate_rate
|
|
self.duplicate_count = duplicate_count
|
|
self.segments = segments
|
|
self.visual_similarity = visual_similarity
|
|
self.match_count = match_count
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
def mark_failed(self, error_message: str) -> None:
|
|
self.status = "failed"
|
|
self.error_message = error_message
|
|
self.updated_at = datetime.now(timezone.utc)
|
|
|
|
def can_retry(self) -> bool:
|
|
"""
|
|
判断是否可重试。
|
|
|
|
仅 failed 状态的记录允许重试,防止误操作已完成的记录。
|
|
|
|
Returns:
|
|
True 表示可以重试
|
|
"""
|
|
return self.status == "failed"
|
|
|
|
def reset_for_retry(self) -> None:
|
|
"""
|
|
重置记录以重新查重。
|
|
|
|
清空查重结果和错误信息,状态回到 pending。
|
|
调用前应先通过 can_retry() 确认状态合法。
|
|
"""
|
|
self.status = "pending"
|
|
self.duplicate_rate = None
|
|
self.duplicate_count = 0
|
|
self.visual_similarity = None
|
|
self.match_count = None
|
|
self.error_message = ""
|
|
self.segments = []
|
|
self.video_fingerprint = None
|
|
self.updated_at = datetime.now(timezone.utc)
|