"""素材原子片段(Atom Clip)领域实体 — #1970 智能剪辑流程重构。 原子片段是素材的逻辑切分单元,不物理切割视频文件。 每条记录指向某条素材的一段 [start_time, end_time] 区间。 """ from __future__ import annotations import uuid from dataclasses import dataclass, field from datetime import UTC, datetime @dataclass class AssetAtomClip: """素材原子片段。 Attributes: id: 唯一标识。 asset_id: 所属素材 ID。 start_time: 片段起始时间(秒,浮点)。 end_time: 片段结束时间(秒,浮点)。 duration: 片段时长 = end_time - start_time(秒)。 clip_index: 在同一素材内的顺序编号(从 0 开始)。 tags: 继承自素材的标签,JSONB 存储,可为空列表。 scene_change_at: 片段尾部是否对齐了 scdet 镜头切换点(存储该切点的精确时间), 未对齐时为 None。 is_fallback: 是否为兜底逻辑在内存中生成的临时片段(不入库)。 created_at: 创建时间。 """ id: str asset_id: str start_time: float end_time: float duration: float clip_index: int tags: list[str] = field(default_factory=list) ai_tags: dict | None = None scene_change_at: float | None = None is_fallback: bool = False created_at: datetime | None = None def __post_init__(self): if not self.id: self.id = str(uuid.uuid4()) if self.duration <= 0: self.duration = round(self.end_time - self.start_time, 3) if self.duration < 0: raise ValueError(f"duration must be >= 0, got start={self.start_time}, end={self.end_time}") if self.start_time < 0: raise ValueError(f"start_time must be >= 0, got {self.start_time}") if self.end_time <= self.start_time: raise ValueError(f"end_time must be > start_time, got start={self.start_time}, end={self.end_time}") if self.clip_index < 0: raise ValueError(f"clip_index must be >= 0, got {self.clip_index}") if self.created_at is None: self.created_at = datetime.now(UTC) @classmethod def create( cls, asset_id: str, start_time: float, end_time: float, clip_index: int, tags: list[str] | None = None, scene_change_at: float | None = None, is_fallback: bool = False, ) -> AssetAtomClip: """工厂方法:创建一个新的原子片段。""" return cls( id="", # __post_init__ 会自动生成 asset_id=asset_id, start_time=round(start_time, 3), end_time=round(end_time, 3), duration=round(end_time - start_time, 3), clip_index=clip_index, tags=tags or [], scene_change_at=scene_change_at, is_fallback=is_fallback, )