52ff2f80ad
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
261 lines
8.8 KiB
Python
261 lines
8.8 KiB
Python
"""Template use cases."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Optional
|
|
|
|
from packages.application.template.commands import (
|
|
CreateCategoryCommand,
|
|
CreateTemplateCommand,
|
|
UpdateTemplateCommand,
|
|
ValidateTemplateCommand,
|
|
)
|
|
from packages.domain.editing_mode import EditingMode
|
|
from packages.domain.template import Template, TemplateCategory, TemplateSegment
|
|
from packages.ports.template_repository import TemplateRepositoryPort
|
|
|
|
|
|
class NotFoundError(Exception):
|
|
pass
|
|
|
|
|
|
class ValidationError(Exception):
|
|
"""业务规则校验失败."""
|
|
|
|
pass
|
|
|
|
|
|
VALID_MODES = {m.value for m in EditingMode}
|
|
VALID_MATERIAL_TYPES = {"人物", "场景"}
|
|
|
|
|
|
@dataclass
|
|
class GenerateWarning:
|
|
"""生成时的警告信息."""
|
|
|
|
code: str # voiceover_duration_mismatch / missing_material_type / ...
|
|
message: str
|
|
details: dict = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class ValidateResult:
|
|
"""模板校验结果."""
|
|
|
|
template: Template
|
|
warnings: List[GenerateWarning] = field(default_factory=list)
|
|
|
|
|
|
# ── Template CRUD ──
|
|
|
|
|
|
class CreateTemplateUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, command: CreateTemplateCommand) -> Template:
|
|
if command.mode not in VALID_MODES:
|
|
raise ValidationError(f"无效的剪辑模式: {command.mode},可选值: {VALID_MODES}")
|
|
|
|
template_id = uuid.uuid4().hex
|
|
template = Template(
|
|
id=template_id,
|
|
user_id=command.user_id,
|
|
name=command.name,
|
|
mode=command.mode,
|
|
category=command.category,
|
|
tags=command.tags,
|
|
title_config=command.title_config,
|
|
subtitle_config=command.subtitle_config,
|
|
bgm_config=command.bgm_config,
|
|
estimated_duration=command.estimated_duration,
|
|
)
|
|
template = self.repository.create(template)
|
|
|
|
# 始终调用 create_segments 以确保在同一事务中提交
|
|
segments = [
|
|
TemplateSegment(
|
|
id=uuid.uuid4().hex,
|
|
template_id=template.id,
|
|
segment_order=seg.segment_order,
|
|
duration_min=seg.duration_min,
|
|
duration_max=seg.duration_max,
|
|
material_type=seg.material_type,
|
|
)
|
|
for seg in command.segments
|
|
]
|
|
self.repository.create_segments(segments)
|
|
template.segments = segments
|
|
|
|
return template
|
|
|
|
|
|
class ListTemplatesUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(
|
|
self,
|
|
user_id: str,
|
|
*,
|
|
skip: int = 0,
|
|
limit: int = 50,
|
|
) -> List[Template]:
|
|
return self.repository.list_by_user(user_id, skip=skip, limit=limit)
|
|
|
|
|
|
class GetTemplateUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, template_id: str, user_id: str) -> Optional[Template]:
|
|
return self.repository.get(template_id, user_id)
|
|
|
|
|
|
class UpdateTemplateUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, command: UpdateTemplateCommand) -> Template:
|
|
existing = self.repository.get(command.template_id, command.user_id)
|
|
if existing is None:
|
|
raise NotFoundError(f"Template {command.template_id} not found")
|
|
|
|
if command.mode is not None and command.mode not in VALID_MODES:
|
|
raise ValidationError(f"无效的剪辑模式: {command.mode}")
|
|
|
|
if command.name is not None:
|
|
existing.name = command.name
|
|
if command.mode is not None:
|
|
existing.mode = command.mode
|
|
if command.category is not None:
|
|
existing.category = command.category
|
|
if command.tags is not None:
|
|
existing.tags = command.tags
|
|
if command.title_config is not None:
|
|
existing.title_config = command.title_config
|
|
if command.subtitle_config is not None:
|
|
existing.subtitle_config = command.subtitle_config
|
|
if command.bgm_config is not None:
|
|
existing.bgm_config = command.bgm_config
|
|
if command.estimated_duration is not None:
|
|
existing.estimated_duration = command.estimated_duration
|
|
|
|
self.repository.update(existing)
|
|
|
|
# Replace segments if provided
|
|
if command.segments is not None:
|
|
self.repository.delete_segments_by_template(existing.id)
|
|
segments = [
|
|
TemplateSegment(
|
|
id=uuid.uuid4().hex,
|
|
template_id=existing.id,
|
|
segment_order=seg.segment_order,
|
|
duration_min=seg.duration_min,
|
|
duration_max=seg.duration_max,
|
|
material_type=seg.material_type,
|
|
)
|
|
for seg in command.segments
|
|
]
|
|
self.repository.create_segments(segments)
|
|
existing.segments = segments
|
|
else:
|
|
existing.segments = self.repository.list_segments(existing.id)
|
|
|
|
return existing
|
|
|
|
|
|
class DeleteTemplateUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, template_id: str, user_id: str) -> bool:
|
|
return self.repository.delete(template_id, user_id)
|
|
|
|
|
|
# ── Validate template ──
|
|
|
|
|
|
class ValidateTemplateUseCase:
|
|
"""校验模板业务规则."""
|
|
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, command: ValidateTemplateCommand) -> ValidateResult:
|
|
template = self.repository.get(command.template_id, command.user_id)
|
|
if template is None:
|
|
raise NotFoundError(f"Template {command.template_id} not found")
|
|
|
|
warnings: List[GenerateWarning] = []
|
|
|
|
# 业务规则 1: one_take 必须恰好 1 个片段
|
|
if template.mode == EditingMode.ONE_TAKE.value:
|
|
if len(template.segments) != 1:
|
|
raise ValidationError(f"一镜到底模式必须恰好有 1 个片段,当前有 {len(template.segments)} 个")
|
|
|
|
# 业务规则 2: voice_over 每个片段必须有 material_type
|
|
if template.mode == EditingMode.VOICE_OVER.value:
|
|
for seg in template.segments:
|
|
if not seg.material_type or seg.material_type not in VALID_MATERIAL_TYPES:
|
|
raise ValidationError(
|
|
f"口播+B-roll模式下每个片段必须指定 material_type(人物/场景),"
|
|
f"片段 {seg.segment_order} 的 material_type 无效: {seg.material_type}"
|
|
)
|
|
|
|
# 业务规则 3: 配音时长偏差 ±30% 警告
|
|
if command.voiceover_duration is not None and template.estimated_duration > 0:
|
|
ratio = command.voiceover_duration / template.estimated_duration
|
|
if ratio < 0.7 or ratio > 1.3:
|
|
warnings.append(
|
|
GenerateWarning(
|
|
code="voiceover_duration_mismatch",
|
|
message=(
|
|
f"配音时长 ({command.voiceover_duration:.1f}s) "
|
|
f"与预估时长 ({template.estimated_duration:.1f}s) "
|
|
f"偏差超过 ±30%,可能影响剪辑效果"
|
|
),
|
|
details={
|
|
"voiceover_duration": command.voiceover_duration,
|
|
"estimated_duration": template.estimated_duration,
|
|
"ratio": round(ratio, 3),
|
|
},
|
|
)
|
|
)
|
|
|
|
return ValidateResult(template=template, warnings=warnings)
|
|
|
|
|
|
# ── Category CRUD ──
|
|
|
|
|
|
class CreateCategoryUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, command: CreateCategoryCommand) -> TemplateCategory:
|
|
category = TemplateCategory(
|
|
id=uuid.uuid4().hex,
|
|
user_id=command.user_id,
|
|
name=command.name,
|
|
)
|
|
return self.repository.create_category(category)
|
|
|
|
|
|
class ListCategoriesUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, user_id: str) -> List[TemplateCategory]:
|
|
return self.repository.list_categories(user_id)
|
|
|
|
|
|
class DeleteCategoryUseCase:
|
|
def __init__(self, repository: TemplateRepositoryPort) -> None:
|
|
self.repository = repository
|
|
|
|
def execute(self, category_id: str, user_id: str) -> bool:
|
|
return self.repository.delete_category(category_id, user_id)
|