chore: squash merge feature/smart-edit-plan into develop (resolve conflicts)
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Waiting to run
Tests / test (pull_request) Waiting to run
Tests / lint (pull_request) Waiting to run
CI/CD Pipeline / Validate Code Quality And Tests (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
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Waiting to run
Tests / test (pull_request) Waiting to run
Tests / lint (pull_request) Waiting to run
CI/CD Pipeline / Validate Code Quality And Tests (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
- Resolve CI/CD and deploy workflow conflicts with develop version - Resolve database and dependencies conflicts with develop version - Keep new edit plan generator and API functionality from feature branch
This commit is contained in:
@@ -9,7 +9,17 @@ from app.dependencies import (
|
||||
get_project_repository,
|
||||
get_workspace_member_repository,
|
||||
)
|
||||
from app.schemas.edit_plan import CreateEditPlanRequest, EditPlanClipResponse, EditPlanResponse, EditTemplateResponse
|
||||
from app.schemas.edit_plan import (
|
||||
AutoGenerateEditPlanRequest,
|
||||
CreateEditPlanRequest,
|
||||
EditPlanClipResponse,
|
||||
EditPlanResponse,
|
||||
EditTemplateResponse,
|
||||
)
|
||||
from apps.worker.worker_app.tasks.edit_plan_generator import (
|
||||
EditingMode,
|
||||
SmartEditPlanGenerator,
|
||||
)
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -81,6 +91,7 @@ def _to_plan_response(
|
||||
title_id=plan.title_id,
|
||||
status=plan.status,
|
||||
summary=plan.summary,
|
||||
editing_mode=plan.editing_mode,
|
||||
clips=[
|
||||
EditPlanClipResponse(
|
||||
id=clip.id,
|
||||
@@ -90,6 +101,7 @@ def _to_plan_response(
|
||||
start_time=float(clip.start_time or 0),
|
||||
duration=float(clip.duration or 0),
|
||||
reason=clip.reason,
|
||||
layer=clip.layer,
|
||||
)
|
||||
for clip in clips
|
||||
],
|
||||
@@ -98,7 +110,7 @@ def _to_plan_response(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/templates", response_model=list[EditTemplateResponse])
|
||||
@router.get("/templates/", response_model=list[EditTemplateResponse])
|
||||
def list_edit_templates(
|
||||
project_id: str,
|
||||
workspace_id: str,
|
||||
@@ -178,6 +190,98 @@ def create_edit_plan(
|
||||
return _to_plan_response(plan, clips, {asset.id: asset.name for asset in selected})
|
||||
|
||||
|
||||
@router.post("/auto-generate", response_model=EditPlanResponse)
|
||||
def auto_generate_edit_plan(
|
||||
project_id: str,
|
||||
request: AutoGenerateEditPlanRequest,
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
project_repository=Depends(get_project_repository),
|
||||
asset_repository=Depends(get_asset_repository),
|
||||
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
|
||||
session: Session = Depends(get_db_session),
|
||||
) -> EditPlanResponse:
|
||||
"""
|
||||
智能生成剪辑计划
|
||||
|
||||
根据素材的分类结果和质量评分,自动编排剪辑计划。
|
||||
支持多种剪辑模式:
|
||||
- one-take: 按分类分组,组内按质量排序,顺序拼接
|
||||
- pip: 第一个高质量素材为主画面,其余为画中画
|
||||
- voiceover: person 类素材为主播口播,其余穿插为 B-roll
|
||||
- voice_pip: 结合 voiceover 和 pip,第一个高质量 person 素材为主画面
|
||||
"""
|
||||
_ensure_project(
|
||||
project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository
|
||||
)
|
||||
|
||||
# 获取素材库中的所有素材
|
||||
assets = asset_repository.list_by_library(request.asset_library_id)
|
||||
|
||||
if not assets:
|
||||
raise HTTPException(status_code=422, detail="素材库中暂无素材")
|
||||
|
||||
# 使用智能生成器
|
||||
generator = SmartEditPlanGenerator(project_id, assets)
|
||||
|
||||
try:
|
||||
plan_result = generator.generate_plan(
|
||||
editing_mode=request.editing_mode,
|
||||
target_duration=request.target_duration
|
||||
)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
if not plan_result.clips:
|
||||
raise HTTPException(status_code=422, detail="无符合条件的视频素材")
|
||||
|
||||
# 获取模板
|
||||
template = (
|
||||
session.query(EditTemplateModel).filter(EditTemplateModel.id == request.template_id).first()
|
||||
if request.template_id
|
||||
else None
|
||||
)
|
||||
if template is None:
|
||||
template = _default_template(session, request.workspace_id, project_id, authenticated_user.user.id)
|
||||
|
||||
# 创建剪辑计划
|
||||
plan = EditPlanModel(
|
||||
id=uuid4().hex,
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=project_id,
|
||||
template_id=template.id,
|
||||
asset_library_id=request.asset_library_id,
|
||||
title_id=request.title_id or "",
|
||||
status="draft",
|
||||
editing_mode=request.editing_mode,
|
||||
summary=plan_result.summary,
|
||||
created_by_user_id=authenticated_user.user.id,
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
session.add(plan)
|
||||
|
||||
# 创建剪辑片段
|
||||
clips: list[EditPlanClipModel] = []
|
||||
asset_name_map = {asset.id: asset.name for asset in assets}
|
||||
|
||||
for clip_plan in plan_result.clips:
|
||||
clip = EditPlanClipModel(
|
||||
id=uuid4().hex,
|
||||
edit_plan_id=plan.id,
|
||||
asset_id=clip_plan.asset_id,
|
||||
sequence=clip_plan.sequence,
|
||||
start_time=clip_plan.start_time,
|
||||
duration=clip_plan.duration,
|
||||
reason=clip_plan.reason,
|
||||
layer=clip_plan.layer,
|
||||
)
|
||||
session.add(clip)
|
||||
clips.append(clip)
|
||||
|
||||
session.commit()
|
||||
|
||||
return _to_plan_response(plan, clips, asset_name_map)
|
||||
|
||||
|
||||
@router.get("/{plan_id}", response_model=EditPlanResponse)
|
||||
def get_edit_plan(
|
||||
project_id: str,
|
||||
|
||||
@@ -23,6 +23,7 @@ class EditPlanClipResponse(BaseModel):
|
||||
start_time: float
|
||||
duration: float
|
||||
reason: str
|
||||
layer: str = "main" # main, pip, broll
|
||||
|
||||
|
||||
class EditPlanResponse(BaseModel):
|
||||
@@ -34,6 +35,7 @@ class EditPlanResponse(BaseModel):
|
||||
title_id: str = ""
|
||||
status: str
|
||||
summary: str
|
||||
editing_mode: str | None = None # one-take, pip, voiceover, voice_pip
|
||||
clips: list[EditPlanClipResponse] = Field(default_factory=list)
|
||||
created_at: datetime | None = None
|
||||
updated_at: datetime | None = None
|
||||
@@ -44,3 +46,19 @@ class CreateEditPlanRequest(BaseModel):
|
||||
asset_library_id: str
|
||||
template_id: str = ""
|
||||
title_id: str = ""
|
||||
|
||||
|
||||
class AutoGenerateEditPlanRequest(BaseModel):
|
||||
"""智能生成剪辑计划请求"""
|
||||
workspace_id: str
|
||||
asset_library_id: str
|
||||
editing_mode: str = Field(
|
||||
default="one-take",
|
||||
description="剪辑模式: one-take, pip, voiceover, voice_pip"
|
||||
)
|
||||
target_duration: float = Field(
|
||||
default=30.0,
|
||||
description="目标时长(秒)"
|
||||
)
|
||||
template_id: str = ""
|
||||
title_id: str = ""
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
"""Smart Edit Plan Generator - 根据分类和质量评分智能编排剪辑计划"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from packages.domain import Asset, AssetClassification, AssetStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EditingMode(StrEnum):
|
||||
"""剪辑模式枚举"""
|
||||
ONE_TAKE = "one-take" # 顺序拼接模式
|
||||
PIP = "pip" # 画中画模式
|
||||
VOICEOVER = "voiceover" # 口播+B-roll模式
|
||||
VOICE_PIP = "voice_pip" # 口播+画中画组合模式
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditClipPlan:
|
||||
"""单个剪辑片段的编排计划"""
|
||||
asset_id: str
|
||||
sequence: int
|
||||
start_time: float = 0.0
|
||||
duration: float = 0.0
|
||||
layer: str = "main" # main, pip, broll
|
||||
reason: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class EditPlanResult:
|
||||
"""完整剪辑计划结果"""
|
||||
project_id: str
|
||||
editing_mode: EditingMode
|
||||
clips: list[EditClipPlan]
|
||||
total_duration: float
|
||||
summary: str
|
||||
|
||||
|
||||
def _calculate_start_times(clips: list[EditClipPlan]) -> list[EditClipPlan]:
|
||||
"""
|
||||
计算时间轴,根据前面的片段时长累加 start_time
|
||||
|
||||
Args:
|
||||
clips: 已按 sequence 排序的片段列表
|
||||
|
||||
Returns:
|
||||
修正后的片段列表
|
||||
"""
|
||||
current_time = 0.0
|
||||
for clip in clips:
|
||||
clip.start_time = current_time
|
||||
current_time += clip.duration
|
||||
return clips
|
||||
|
||||
|
||||
class SmartEditPlanGenerator:
|
||||
"""
|
||||
智能剪辑计划生成器
|
||||
|
||||
根据素材的分类结果和质量评分,自动编排剪辑计划。
|
||||
支持多种剪辑模式:one-take, pip, voiceover, voice_pip
|
||||
"""
|
||||
|
||||
def __init__(self, project_id: str, assets: list[Asset]):
|
||||
self.project_id = project_id
|
||||
# 筛选已就绪的视频素材
|
||||
self.assets = [
|
||||
a for a in assets
|
||||
if a.status == AssetStatus.READY and a.mime_type.startswith("video/")
|
||||
]
|
||||
self.assets_by_classification: dict[str, list[Asset]] = defaultdict(list)
|
||||
|
||||
def _parse_classification(self, asset: Asset) -> str:
|
||||
"""解析素材的分类结果"""
|
||||
# 从 metadata 中获取分类
|
||||
classification = asset.metadata.get("classification", "")
|
||||
if not classification:
|
||||
# 尝试从 classification_result 字段获取
|
||||
classification = asset.metadata.get("classification_result", "")
|
||||
|
||||
# 如果是 JSON 字符串,解析它
|
||||
if classification and isinstance(classification, str):
|
||||
try:
|
||||
parsed = json.loads(classification)
|
||||
if isinstance(parsed, dict):
|
||||
classification = parsed.get("classification", "other")
|
||||
elif isinstance(parsed, str):
|
||||
classification = parsed
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
# 验证分类值是否有效
|
||||
valid_classifications = [c.value for c in AssetClassification]
|
||||
if classification not in valid_classifications:
|
||||
classification = "other"
|
||||
|
||||
return classification
|
||||
|
||||
def _group_by_classification(self) -> None:
|
||||
"""按分类结果对素材分组"""
|
||||
for asset in self.assets:
|
||||
classification = self._parse_classification(asset)
|
||||
self.assets_by_classification[classification].append(asset)
|
||||
|
||||
def _sort_by_quality(self, assets: list[Asset]) -> list[Asset]:
|
||||
"""按质量评分排序,高分在前"""
|
||||
return sorted(
|
||||
assets,
|
||||
key=lambda a: (-(a.quality_score or 0), a.created_at)
|
||||
)
|
||||
|
||||
def _calculate_clip_duration(self, asset: Asset, target_duration: float, clip_count: int) -> float:
|
||||
"""计算单个片段的时长"""
|
||||
if asset.duration:
|
||||
# 如果素材时长超过平均时长,取平均时长
|
||||
avg_duration = target_duration / max(1, clip_count)
|
||||
return min(float(asset.duration), avg_duration)
|
||||
return target_duration / max(1, clip_count)
|
||||
|
||||
def _generate_one_take(self, target_duration: float = 30.0) -> EditPlanResult:
|
||||
"""
|
||||
One-Take 模式:按分类分组,组内按质量排序,顺序拼接
|
||||
"""
|
||||
self._group_by_classification()
|
||||
|
||||
clips: list[EditClipPlan] = []
|
||||
sequence = 1
|
||||
|
||||
# 按优先级排序分类:person > scenic > product > other
|
||||
priority_order = ["person", "scenic", "product", "animal", "food", "tech", "sport", "music", "other"]
|
||||
sorted_classifications = sorted(
|
||||
self.assets_by_classification.keys(),
|
||||
key=lambda c: priority_order.index(c) if c in priority_order else len(priority_order)
|
||||
)
|
||||
|
||||
for classification in sorted_classifications:
|
||||
sorted_assets = self._sort_by_quality(self.assets_by_classification[classification])
|
||||
for asset in sorted_assets:
|
||||
duration = self._calculate_clip_duration(
|
||||
asset, target_duration, len(self.assets)
|
||||
)
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=duration,
|
||||
layer="main",
|
||||
reason=f"按分类 [{classification}] 排列,质量评分 {asset.quality_score or 0:.1f}"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
total_duration = sum(c.duration for c in clips)
|
||||
_calculate_start_times(clips)
|
||||
return EditPlanResult(
|
||||
project_id=self.project_id,
|
||||
editing_mode=EditingMode.ONE_TAKE,
|
||||
clips=clips,
|
||||
total_duration=total_duration,
|
||||
summary=f"One-Take 模式:按 {len(sorted_classifications)} 个分类分组,共 {len(clips)} 段素材"
|
||||
)
|
||||
|
||||
def _generate_pip(self, target_duration: float = 30.0) -> EditPlanResult:
|
||||
"""
|
||||
PIP 模式:第一个高质量素材为主画面,其余为画中画
|
||||
"""
|
||||
sorted_assets = self._sort_by_quality(self.assets)
|
||||
|
||||
if not sorted_assets:
|
||||
return EditPlanResult(
|
||||
project_id=self.project_id,
|
||||
editing_mode=EditingMode.PIP,
|
||||
clips=[],
|
||||
total_duration=0,
|
||||
summary="无素材可用"
|
||||
)
|
||||
|
||||
clips: list[EditClipPlan] = []
|
||||
sequence = 1
|
||||
|
||||
# 第一个高质量素材作为主画面
|
||||
main_asset = sorted_assets[0]
|
||||
main_duration = min(
|
||||
float(main_asset.duration) if main_asset.duration else target_duration,
|
||||
target_duration
|
||||
)
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=main_asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=main_duration,
|
||||
layer="main",
|
||||
reason=f"高质量主画面 (质量评分: {main_asset.quality_score or 0:.1f})"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
# 其余素材作为画中画
|
||||
for asset in sorted_assets[1:]:
|
||||
duration = self._calculate_clip_duration(asset, target_duration, len(sorted_assets))
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=duration,
|
||||
layer="pip",
|
||||
reason=f"画中画素材 (质量评分: {asset.quality_score or 0:.1f})"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
total_duration = main_duration
|
||||
_calculate_start_times(clips)
|
||||
return EditPlanResult(
|
||||
project_id=self.project_id,
|
||||
editing_mode=EditingMode.PIP,
|
||||
clips=clips,
|
||||
total_duration=total_duration,
|
||||
summary=f"PIP 模式:1 个主画面 + {len(sorted_assets) - 1} 个画中画"
|
||||
)
|
||||
|
||||
def _generate_voiceover(self, target_duration: float = 30.0) -> EditPlanResult:
|
||||
"""
|
||||
Voiceover 模式:person 类素材为主播口播,其余穿插为 B-roll
|
||||
"""
|
||||
self._group_by_classification()
|
||||
|
||||
person_assets = self._sort_by_quality(
|
||||
self.assets_by_classification.get("person", [])
|
||||
)
|
||||
other_assets = self._sort_by_quality([
|
||||
a for assets in self.assets_by_classification.values()
|
||||
for a in assets
|
||||
if self._parse_classification(a) != "person"
|
||||
])
|
||||
|
||||
clips: list[EditClipPlan] = []
|
||||
sequence = 1
|
||||
|
||||
# 合并口播和 B-roll
|
||||
main_assets = person_assets if person_assets else other_assets
|
||||
broll_assets = [a for a in other_assets if a not in person_assets] if person_assets else []
|
||||
|
||||
# 优先使用 person 素材作为口播
|
||||
for i, asset in enumerate(main_assets):
|
||||
duration = self._calculate_clip_duration(asset, target_duration, len(main_assets))
|
||||
is_person = asset in person_assets
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=duration,
|
||||
layer="main" if is_person else "broll",
|
||||
reason=f"{'主播口播' if is_person else 'B-roll'} (质量评分: {asset.quality_score or 0:.1f})"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
# 在口播之间穿插 B-roll
|
||||
if is_person and broll_assets and i < len(main_assets) - 1:
|
||||
broll_asset = broll_assets[i % len(broll_assets)]
|
||||
broll_duration = self._calculate_clip_duration(
|
||||
broll_asset, target_duration, len(main_assets) + len(broll_assets)
|
||||
)
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=broll_asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=broll_duration,
|
||||
layer="broll",
|
||||
reason=f"B-roll 穿插 (质量评分: {broll_asset.quality_score or 0:.1f})"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
total_duration = sum(c.duration for c in clips)
|
||||
person_count = len(person_assets)
|
||||
_calculate_start_times(clips)
|
||||
return EditPlanResult(
|
||||
project_id=self.project_id,
|
||||
editing_mode=EditingMode.VOICEOVER,
|
||||
clips=clips,
|
||||
total_duration=total_duration,
|
||||
summary=f"Voiceover 模式:{person_count} 段口播 + {len(clips) - person_count} 段 B-roll"
|
||||
)
|
||||
|
||||
def _generate_voice_pip(self, target_duration: float = 30.0) -> EditPlanResult:
|
||||
"""
|
||||
Voice-PIP 模式:结合 voiceover 和 pip
|
||||
第一个高质量 person 素材为主画面,其余为 PIP B-roll
|
||||
"""
|
||||
self._group_by_classification()
|
||||
|
||||
person_assets = self._sort_by_quality(
|
||||
self.assets_by_classification.get("person", [])
|
||||
)
|
||||
other_assets = self._sort_by_quality([
|
||||
a for assets in self.assets_by_classification.values()
|
||||
for a in assets
|
||||
if self._parse_classification(a) != "person"
|
||||
])
|
||||
|
||||
clips: list[EditClipPlan] = []
|
||||
sequence = 1
|
||||
|
||||
# 主画面:优先使用高质量 person 素材
|
||||
main_asset = person_assets[0] if person_assets else (other_assets[0] if other_assets else None)
|
||||
if main_asset:
|
||||
main_duration = min(
|
||||
float(main_asset.duration) if main_asset.duration else target_duration,
|
||||
target_duration
|
||||
)
|
||||
is_person = main_asset in person_assets
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=main_asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=main_duration,
|
||||
layer="main",
|
||||
reason=f"{'主播口播' if is_person else '主画面'} (质量评分: {main_asset.quality_score or 0:.1f})"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
# PIP 素材
|
||||
pip_assets = [a for a in (person_assets[1:] + other_assets) if a != main_asset]
|
||||
for asset in pip_assets:
|
||||
duration = self._calculate_clip_duration(asset, target_duration, len(pip_assets) + 1)
|
||||
clips.append(EditClipPlan(
|
||||
asset_id=asset.id,
|
||||
sequence=sequence,
|
||||
start_time=0,
|
||||
duration=duration,
|
||||
layer="pip",
|
||||
reason=f"PIP 素材 (质量评分: {asset.quality_score or 0:.1f})"
|
||||
))
|
||||
sequence += 1
|
||||
|
||||
total_duration = sum(c.duration for c in clips)
|
||||
_calculate_start_times(clips)
|
||||
return EditPlanResult(
|
||||
project_id=self.project_id,
|
||||
editing_mode=EditingMode.VOICE_PIP,
|
||||
clips=clips,
|
||||
total_duration=total_duration,
|
||||
summary=f"Voice-PIP 模式:1 个主画面 + {len(pip_assets)} 个 PIP 素材"
|
||||
)
|
||||
|
||||
def generate_plan(
|
||||
self,
|
||||
editing_mode: str = "one-take",
|
||||
target_duration: float = 30.0
|
||||
) -> EditPlanResult:
|
||||
"""
|
||||
生成剪辑计划
|
||||
|
||||
Args:
|
||||
editing_mode: 剪辑模式 (one-take/pip/voiceover/voice_pip)
|
||||
target_duration: 目标时长(秒)
|
||||
|
||||
Returns:
|
||||
EditPlanResult: 编排好的剪辑计划
|
||||
"""
|
||||
logger.info(f"Generating edit plan for project {self.project_id} with mode {editing_mode}")
|
||||
|
||||
if not self.assets:
|
||||
logger.warning(f"No ready video assets found for project {self.project_id}")
|
||||
return EditPlanResult(
|
||||
project_id=self.project_id,
|
||||
editing_mode=EditingMode(editing_mode),
|
||||
clips=[],
|
||||
total_duration=0,
|
||||
summary="无素材可用"
|
||||
)
|
||||
|
||||
mode = EditingMode(editing_mode.lower())
|
||||
|
||||
if mode == EditingMode.ONE_TAKE:
|
||||
return self._generate_one_take(target_duration)
|
||||
elif mode == EditingMode.PIP:
|
||||
return self._generate_pip(target_duration)
|
||||
elif mode == EditingMode.VOICEOVER:
|
||||
return self._generate_voiceover(target_duration)
|
||||
elif mode == EditingMode.VOICE_PIP:
|
||||
return self._generate_voice_pip(target_duration)
|
||||
else:
|
||||
raise ValueError(f"Unknown editing mode: {editing_mode}")
|
||||
@@ -156,6 +156,7 @@ class EditPlanModel(Base):
|
||||
template_id = Column(String(32), nullable=False, index=True)
|
||||
asset_library_id = Column(String(32), nullable=False, index=True)
|
||||
title_id = Column(String(32), nullable=False, default="")
|
||||
editing_mode = Column(String(20), nullable=True, default=None, index=True)
|
||||
status = Column(String(20), nullable=False, default="draft", index=True)
|
||||
summary = Column(Text, nullable=False, default="")
|
||||
created_by_user_id = Column(String(32), nullable=False, default="")
|
||||
@@ -173,6 +174,7 @@ class EditPlanClipModel(Base):
|
||||
start_time = Column(Float, nullable=False, default=0)
|
||||
duration = Column(Float, nullable=False, default=0)
|
||||
reason = Column(Text, nullable=False, default="")
|
||||
layer = Column(String(20), nullable=False, default="main")
|
||||
|
||||
|
||||
class IngestJobModel(Base):
|
||||
@@ -247,6 +249,9 @@ class GeneratedVideoModel(Base):
|
||||
generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime, nullable=True)
|
||||
video_fingerprint = Column(Text, nullable=True)
|
||||
is_duplicate = Column(Boolean, nullable=False, default=False)
|
||||
duplicate_of = Column(String(32), nullable=True)
|
||||
|
||||
|
||||
class TaskModel(Base):
|
||||
|
||||
Reference in New Issue
Block a user