ee8c60a143
1. 修复 hamming_distance 不等长哈希处理(hex() 前导零丢失) 2. 集成颜色直方图到 check_duplicate(此前计算但未使用,浪费 CPU) 3. check_duplicate 改为返回最佳匹配而非首个匹配 4. 修复仓库删除顺序(先删片段再删记录,防止孤儿数据) 5. 域模型添加 can_retry()/reset_for_retry(),仅 failed 状态允许重试 6. 列表接口暴露 offset/limit 分页参数
140 lines
4.4 KiB
Python
140 lines
4.4 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
|
|
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]) -> 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.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.error_message = ""
|
|
self.segments = []
|
|
self.video_fingerprint = None
|
|
self.updated_at = datetime.now(timezone.utc)
|