Files
xiaoxia-saas/packages/domain/edit_plan_clip.py
T
xiaoxia 316f01b3f0
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m6s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m10s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m2s
CI/CD Pipeline / Unit Tests (push) Successful in 15m50s
CI/CD Pipeline / Integration Tests (push) Successful in 5m20s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m42s
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 / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m29s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m17s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m41s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
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 / Staging API Integration Tests (push) Failing after 49s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 56s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m59s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
feat: 预览生成支持随机选素材 + 随机截取片段 (#1256) (#1268)
2026-08-07 19:50:01 +08:00

174 lines
6.0 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" # 渲染失败
@classmethod
def _missing_(cls, value: object) -> "EditPlanClipStatus":
"""兼容历史脏数据,避免枚举转换失败导致500。
- success/done/finished/complete/rendered → RENDERED
- fail/error/err → FAILED
- ready/available → READY
- 其他未知值 → PENDING(兜底,不阻塞业务)
"""
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in ("done", "success", "finished", "complete", "rendered", "render"):
return cls.RENDERED
if normalized in ("fail", "failed", "error", "err"):
return cls.FAILED
if normalized in ("ready", "available", "prepared"):
return cls.READY
return cls.PENDING
@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, *, start_time: float | None = None) -> None:
"""分配素材
Args:
asset_id: 素材 ID
start_time: 可选,素材播放起始时间(秒)。如果提供且在有效范围内,则设置;否则保持默认 0.0
"""
if not asset_id.strip():
raise ValueError("asset_id 不能为空")
self.asset_id = asset_id.strip()
if start_time is not None and start_time >= 0:
self.start_time = start_time
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)