Files
xiaoxia-saas/packages/domain/edit_plan_clip.py
T
xiaoxia 2081c72be6
CI/CD Pipeline / Unit Tests (push) Successful in 1m8s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m18s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 2m3s
CI/CD Pipeline / Integration Tests (push) Successful in 1m12s
CI/CD Pipeline / Build Production Runtime Images (push) Successful in 5m31s
CI/CD Pipeline / Deploy Production (push) Failing after 11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
feat: 视频调速引擎(快进/慢放) (#294)
2026-07-14 11:36:24 +08:00

148 lines
4.9 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""EditPlanClip domain entity for Phase 8 模板编排引擎.
剪辑计划中的具体片段实例,关联 EditPlan 和 TemplateClipConfig
包含实际素材、实际文案、排序和时长等信息。
"""
from __future__ import annotations
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
pass
from typing import Any
from uuid import uuid4
class EditPlanClipStatus(StrEnum):
"""片段状态"""
PENDING = "pending" # 待处理(素材未就绪)
READY = "ready" # 就绪(素材已就绪,可渲染)
RENDERED = "rendered" # 已渲染
FAILED = "failed" # 渲染失败
@dataclass(slots=True)
class EditPlanClip:
"""剪辑计划片段
表示 EditPlan 中的一个具体片段实例,包含实际素材、文案和渲染状态。
可选地关联 TemplateClipConfig 以继承模板规则。
"""
id: str
plan_id: str
clip_type: str
order: int
template_clip_config_id: str = ""
asset_id: str = ""
text_content: str = ""
start_time: float = 0.0
duration: float = 0.0
transition_effect: str = "cut"
transition_duration: float = 0.0 # 0 表示使用全局默认值
playback_speed: float = 1.0 # 0 或 1.0 表示原速,范围 0.25~4.0
status: EditPlanClipStatus = EditPlanClipStatus.PENDING
config: dict[str, Any] = field(default_factory=dict)
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,
plan_id: str,
clip_type: str,
order: int,
*,
template_clip_config_id: str = "",
asset_id: str = "",
text_content: str = "",
start_time: float = 0.0,
duration: float = 0.0,
transition_effect: str = "cut",
transition_duration: float = 0.0,
playback_speed: float = 1.0,
config: dict[str, Any] | None = None,
) -> EditPlanClip:
"""创建剪辑计划片段"""
if not plan_id.strip():
raise ValueError("plan_id 不能为空")
if not clip_type.strip():
raise ValueError("clip_type 不能为空")
if start_time < 0:
raise ValueError("start_time 不能为负数")
if duration < 0:
raise ValueError("duration 不能为负数")
# 速度边界钳制
if playback_speed <= 0:
playback_speed = 1.0
elif playback_speed < 0.25:
playback_speed = 0.25
elif playback_speed > 4.0:
playback_speed = 4.0
return cls(
id=uuid4().hex,
plan_id=plan_id.strip(),
clip_type=clip_type.strip(),
order=order,
template_clip_config_id=template_clip_config_id.strip() if template_clip_config_id else "",
asset_id=asset_id.strip() if asset_id else "",
text_content=text_content.strip(),
start_time=start_time,
duration=duration,
transition_effect=transition_effect.strip() or "cut",
transition_duration=max(0.0, transition_duration),
playback_speed=playback_speed,
status=EditPlanClipStatus.PENDING,
config=config or {},
)
def assign_asset(self, asset_id: str) -> None:
"""分配素材"""
if not asset_id.strip():
raise ValueError("asset_id 不能为空")
self.asset_id = asset_id.strip()
self.updated_at = datetime.now(timezone.utc)
def mark_ready(self) -> None:
"""标记为就绪"""
if self.status != EditPlanClipStatus.PENDING:
raise ValueError(f"只有 pending 状态的片段可以标记就绪,当前状态: {self.status}")
self.status = EditPlanClipStatus.READY
self.updated_at = datetime.now(timezone.utc)
def mark_rendered(self) -> None:
"""标记为已渲染"""
if self.status != EditPlanClipStatus.READY:
raise ValueError(f"只有 ready 状态的片段可以标记已渲染,当前状态: {self.status}")
self.status = EditPlanClipStatus.RENDERED
self.updated_at = datetime.now(timezone.utc)
def mark_failed(self) -> None:
"""标记为失败"""
if self.status != EditPlanClipStatus.READY:
raise ValueError(f"只有 ready 状态的片段可以标记失败,当前状态: {self.status}")
self.status = EditPlanClipStatus.FAILED
self.updated_at = datetime.now(timezone.utc)
@property
def end_time(self) -> float:
"""片段结束时间"""
return self.start_time + self.duration
@property
def has_asset(self) -> bool:
"""是否已分配素材"""
return bool(self.asset_id)