from __future__ import annotations from dataclasses import dataclass, field from datetime import UTC, datetime from typing import Any from uuid import uuid4 @dataclass(slots=True) class GeneratedVideo: id: str project_id: str generation_task_id: str name: str file_url: str file_size: int duration: float width: int height: int fps: float user_id: str = "" thumbnail_url: str | None = None status: str = "completed" review_status: str = "pending_review" generation_params: dict[str, Any] = field(default_factory=dict) video_fingerprint: dict[str, Any] | None = None is_duplicate: bool = False duplicate_of: str | None = None duplicate_rate: float | None = None match_count: int | None = None visual_similarity: float | None = None generated_at: datetime = field(default_factory=lambda: datetime.now(UTC)) created_at: datetime = field(default_factory=lambda: datetime.now(UTC)) @classmethod def create( cls, project_id: str, generation_task_id: str, name: str, file_url: str, *, user_id: str = "", file_size: int = 0, duration: float = 0.0, width: int = 0, height: int = 0, fps: float = 0.0, thumbnail_url: str | None = None, generation_params: dict[str, Any] | None = None, ) -> "GeneratedVideo": # project_id / generation_task_id 允许为空:AI数字人等无项目场景下,前端可能不传 project_id; # lipsync 路径下 generation_task_id 也可能暂时为空。空串会被下面统一兜底为 "" 入库。 if not name or not name.strip(): raise ValueError("name cannot be empty") if not file_url or not file_url.strip(): raise ValueError("file_url cannot be empty") return cls( id=uuid4().hex, project_id=(project_id or "").strip(), user_id=(user_id or "").strip(), generation_task_id=(generation_task_id or "").strip(), name=name.strip(), file_url=file_url.strip(), file_size=file_size, duration=duration, width=width, height=height, fps=fps, thumbnail_url=thumbnail_url, generation_params=generation_params or {}, )