feat(plans): add edit plan preview workflow

This commit is contained in:
Xiaoxia AI
2026-06-24 10:10:25 +08:00
parent 07c62b0b30
commit 0f59b412bc
16 changed files with 460 additions and 5 deletions
@@ -0,0 +1,59 @@
from alembic import op
import sqlalchemy as sa
revision = "006_add_edit_plan_tables"
down_revision = "005_add_generated_video_management_fields"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"edit_templates",
sa.Column("id", sa.String(32), primary_key=True),
sa.Column("workspace_id", sa.String(32), nullable=False, index=True),
sa.Column("project_id", sa.String(32), nullable=False, index=True),
sa.Column("name", sa.String(120), nullable=False),
sa.Column("description", sa.Text(), nullable=False, server_default=""),
sa.Column("target_duration", sa.Float(), nullable=False, server_default="30"),
sa.Column("clip_count", sa.Integer(), nullable=False, server_default="3"),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
sa.Column("created_by_user_id", sa.String(32), nullable=False, server_default=""),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
)
op.create_table(
"edit_plans",
sa.Column("id", sa.String(32), primary_key=True),
sa.Column("workspace_id", sa.String(32), nullable=False, index=True),
sa.Column("project_id", sa.String(32), nullable=False, index=True),
sa.Column("template_id", sa.String(32), nullable=False, index=True),
sa.Column("asset_library_id", sa.String(32), nullable=False, index=True),
sa.Column("title_id", sa.String(32), nullable=False, server_default=""),
sa.Column("status", sa.String(20), nullable=False, server_default="draft", index=True),
sa.Column("summary", sa.Text(), nullable=False, server_default=""),
sa.Column("created_by_user_id", sa.String(32), nullable=False, server_default=""),
sa.Column("created_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime(), nullable=False, server_default=sa.func.now()),
)
op.create_table(
"edit_plan_clips",
sa.Column("id", sa.String(32), primary_key=True),
sa.Column("edit_plan_id", sa.String(32), nullable=False, index=True),
sa.Column("asset_id", sa.String(32), nullable=False, index=True),
sa.Column("sequence", sa.Integer(), nullable=False),
sa.Column("start_time", sa.Float(), nullable=False, server_default="0"),
sa.Column("duration", sa.Float(), nullable=False, server_default="0"),
sa.Column("reason", sa.Text(), nullable=False, server_default=""),
)
op.add_column("generation_tasks", sa.Column("edit_plan_id", sa.String(32), nullable=False, server_default=""))
op.create_index("ix_generation_tasks_edit_plan_id", "generation_tasks", ["edit_plan_id"])
def downgrade() -> None:
op.drop_index("ix_generation_tasks_edit_plan_id", table_name="generation_tasks")
op.drop_column("generation_tasks", "edit_plan_id")
op.drop_table("edit_plan_clips")
op.drop_table("edit_plans")
op.drop_table("edit_templates")
+4
View File
@@ -3,6 +3,7 @@ from app.api.routes.asset_libraries import router as asset_libraries_router
from app.api.routes.assets import router as assets_router
from app.api.routes.auth import router as auth_router
from app.api.routes.classification_jobs import router as classification_jobs_router
from app.api.routes.edit_plans import router as edit_plans_router
from app.api.routes.generated_videos import router as generated_videos_router
from app.api.routes.generation_tasks import router as generation_tasks_router
from app.api.routes.health import router as health_check_router
@@ -36,6 +37,9 @@ api_router.include_router(
project_titles_router,
tags=["标题库"],
)
api_router.include_router(
edit_plans_router,
)
api_router.include_router(
task_center_router,
tags=["任务中心"],
+166
View File
@@ -0,0 +1,166 @@
from datetime import datetime, timezone
from uuid import uuid4
from app.api.routes.permissions import require_workspace_member
from app.auth import AuthenticatedUser, get_current_user
from app.dependencies import get_asset_repository, get_db_session, get_project_repository, get_workspace_member_repository
from app.schemas.edit_plan import CreateEditPlanRequest, EditPlanClipResponse, EditPlanResponse, EditTemplateResponse
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from packages.adapters.sqlalchemy_impl.models import EditPlanClipModel, EditPlanModel, EditTemplateModel
from packages.domain import AssetStatus
from packages.ports.workspace_member_repository import WorkspaceMemberRepository
router = APIRouter(prefix="/projects/{project_id}/edit-plans", tags=["剪辑计划"])
def _ensure_project(project_id: str, workspace_id: str, user: AuthenticatedUser, project_repository, member_repository):
project = project_repository.find_by_id(project_id)
if project is None or project.workspace_id != workspace_id:
raise HTTPException(status_code=404, detail="Project not found")
require_workspace_member(workspace_id, user, member_repository)
return project
def _default_template(session: Session, workspace_id: str, project_id: str, user_id: str) -> EditTemplateModel:
template = session.query(EditTemplateModel).filter(
EditTemplateModel.project_id == project_id,
EditTemplateModel.is_active.is_(True),
).order_by(EditTemplateModel.created_at.asc()).first()
if template is not None:
return template
template = EditTemplateModel(
id=uuid4().hex,
workspace_id=workspace_id,
project_id=project_id,
name="基础节奏模板",
description="自动选择可用视频素材,按上传顺序生成三段式剪辑计划。",
target_duration=30,
clip_count=3,
created_by_user_id=user_id,
)
session.add(template)
session.commit()
return template
def _to_template_response(template: EditTemplateModel) -> EditTemplateResponse:
return EditTemplateResponse(
id=template.id,
workspace_id=template.workspace_id,
project_id=template.project_id,
name=template.name,
description=template.description,
target_duration=float(template.target_duration or 0),
clip_count=int(template.clip_count or 0),
is_active=bool(template.is_active),
created_at=template.created_at,
)
def _to_plan_response(plan: EditPlanModel, clips: list[EditPlanClipModel], asset_names: dict[str, str]) -> EditPlanResponse:
return EditPlanResponse(
id=plan.id,
workspace_id=plan.workspace_id,
project_id=plan.project_id,
template_id=plan.template_id,
asset_library_id=plan.asset_library_id,
title_id=plan.title_id,
status=plan.status,
summary=plan.summary,
clips=[EditPlanClipResponse(
id=clip.id,
asset_id=clip.asset_id,
asset_name=asset_names.get(clip.asset_id, clip.asset_id),
sequence=clip.sequence,
start_time=float(clip.start_time or 0),
duration=float(clip.duration or 0),
reason=clip.reason,
) for clip in clips],
created_at=plan.created_at,
updated_at=plan.updated_at,
)
@router.get("/templates", response_model=list[EditTemplateResponse])
def list_edit_templates(
project_id: str,
workspace_id: str,
authenticated_user: AuthenticatedUser = Depends(get_current_user),
project_repository=Depends(get_project_repository),
workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository),
session: Session = Depends(get_db_session),
) -> list[EditTemplateResponse]:
_ensure_project(project_id, workspace_id, authenticated_user, project_repository, workspace_member_repository)
template = _default_template(session, workspace_id, project_id, authenticated_user.user.id)
templates = session.query(EditTemplateModel).filter(EditTemplateModel.project_id == project_id, EditTemplateModel.is_active.is_(True)).all()
return [_to_template_response(item) for item in templates or [template]]
@router.post("", response_model=EditPlanResponse)
def create_edit_plan(
project_id: str,
request: CreateEditPlanRequest,
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:
_ensure_project(project_id, request.workspace_id, authenticated_user, project_repository, workspace_member_repository)
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)
assets = [asset for asset in asset_repository.list_by_library(request.asset_library_id) if asset.status == AssetStatus.READY and asset.mime_type.startswith("video/")]
if not assets:
raise HTTPException(status_code=422, detail="素材库暂无可用于剪辑计划的视频素材")
selected = sorted(assets, key=lambda asset: (-(asset.quality_score or 0), asset.created_at))[: max(1, int(template.clip_count or 3))]
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,
status="draft",
summary=f"按《{template.name}》自动选择 {len(selected)} 段素材,预计生成约 {int(template.target_duration or 30)} 秒成片。",
created_by_user_id=authenticated_user.user.id,
updated_at=datetime.now(timezone.utc),
)
session.add(plan)
clips: list[EditPlanClipModel] = []
clip_duration = max(1, float(template.target_duration or 30) / len(selected))
for index, asset in enumerate(selected, start=1):
clip = EditPlanClipModel(
id=uuid4().hex,
edit_plan_id=plan.id,
asset_id=asset.id,
sequence=index,
start_time=0,
duration=min(float(asset.duration or clip_duration), clip_duration),
reason="优先选择已就绪、质量分较高的视频素材。",
)
session.add(clip)
clips.append(clip)
session.commit()
return _to_plan_response(plan, clips, {asset.id: asset.name for asset in selected})
@router.get("/{plan_id}", response_model=EditPlanResponse)
def get_edit_plan(
project_id: str,
plan_id: str,
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:
plan = session.query(EditPlanModel).filter(EditPlanModel.id == plan_id, EditPlanModel.project_id == project_id).first()
if plan is None:
raise HTTPException(status_code=404, detail="Edit plan not found")
_ensure_project(project_id, plan.workspace_id, authenticated_user, project_repository, workspace_member_repository)
clips = session.query(EditPlanClipModel).filter(EditPlanClipModel.edit_plan_id == plan.id).order_by(EditPlanClipModel.sequence.asc()).all()
assets = asset_repository.list_by_library(plan.asset_library_id)
return _to_plan_response(plan, clips, {asset.id: asset.name for asset in assets})
@@ -41,6 +41,7 @@ def _to_generation_task_response(task) -> GenerationTaskResponse:
asset_library_id=task.asset_library_id,
strategy_id=task.strategy_id,
voice_library_id=task.voice_library_id,
edit_plan_id=task.edit_plan_id,
status=task.status.value,
progress=task.progress,
result_count=task.result_count,
@@ -114,6 +115,7 @@ def create_generation_task(
asset_library_id=request.asset_library_id,
strategy_id=request.strategy_id or _select_title_id(project_title_repository, request.project_id),
voice_library_id=request.voice_library_id,
edit_plan_id=request.edit_plan_id,
created_by_user_id=authenticated_user.user.id,
)
)
+1
View File
@@ -130,6 +130,7 @@ def retry_project_task(
asset_library_id=task.asset_library_id,
strategy_id=task.strategy_id,
voice_library_id=task.voice_library_id,
edit_plan_id=task.edit_plan_id,
created_by_user_id=authenticated_user.user.id,
))
celery_app.send_task("worker.generate_video", args=[retried.id])
+45
View File
@@ -0,0 +1,45 @@
from datetime import datetime
from pydantic import BaseModel, Field
class EditTemplateResponse(BaseModel):
id: str
workspace_id: str
project_id: str
name: str
description: str
target_duration: float
clip_count: int
is_active: bool
created_at: datetime | None = None
class EditPlanClipResponse(BaseModel):
id: str
asset_id: str
asset_name: str
sequence: int
start_time: float
duration: float
reason: str
class EditPlanResponse(BaseModel):
id: str
workspace_id: str
project_id: str
template_id: str
asset_library_id: str
title_id: str = ""
status: str
summary: str
clips: list[EditPlanClipResponse] = Field(default_factory=list)
created_at: datetime | None = None
updated_at: datetime | None = None
class CreateEditPlanRequest(BaseModel):
workspace_id: str
asset_library_id: str
template_id: str = ""
title_id: str = ""
+2
View File
@@ -7,6 +7,7 @@ class CreateGenerationTaskRequest(BaseModel):
asset_library_id: str = Field(..., min_length=1)
strategy_id: str = ""
voice_library_id: str = ""
edit_plan_id: str = ""
created_by_user_id: str = ""
@@ -17,6 +18,7 @@ class GenerationTaskResponse(BaseModel):
asset_library_id: str
strategy_id: str
voice_library_id: str
edit_plan_id: str
status: str
progress: float
result_count: int
+10 -3
View File
@@ -11,7 +11,7 @@ type WorkspaceResponse = { id?: string; workspace_id?: string };
type ProjectResponse = { id: string };
type LibraryResponse = { id: string };
type AssetListResponse = { items: Array<{ name: string; status: string; mime_type?: string; file_type?: string }> };
type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null; strategy_id?: string | null };
type GenerationTaskResponse = { id: string; status: string; progress: number; result_count: number; error_message?: string | null; strategy_id?: string | null; edit_plan_id?: string | null };
type ProjectTitleResponse = { id: string; text: string; usage_count: number };
type GeneratedVideoResponse = { id: string; name: string; file_url: string; file_size: number };
@@ -116,16 +116,22 @@ test.describe('Core generation and download flow', () => {
await page.getByText(projectTitleText).click();
await expect(page.getByText(/素材准备度:/)).toBeVisible({ timeout: 20_000 });
await expect(page.getByRole('button', { name: '发起生成' })).toBeEnabled({ timeout: 20_000 });
await expect(page.getByRole('button', { name: '生成剪辑计划预览' })).toBeEnabled({ timeout: 20_000 });
await page.getByRole('button', { name: '生成剪辑计划预览' }).click();
await expect(page.getByText('剪辑计划预览')).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(/自动选择/)).toBeVisible({ timeout: 20_000 });
await expect(page.getByText(/e2e-generation-source\.mp4/)).toBeVisible({ timeout: 20_000 });
await expect(page.getByRole('button', { name: '确认计划并发起生成' })).toBeEnabled({ timeout: 20_000 });
const createTaskResponsePromise = page.waitForResponse(
(response) => response.url().includes('/api/v1/generation/tasks') && response.request().method() === 'POST',
{ timeout: 30_000 }
);
await page.getByRole('button', { name: '发起生成' }).click();
await page.getByRole('button', { name: '确认计划并发起生成' }).click();
const createTaskResponse = await createTaskResponsePromise;
expect(createTaskResponse.status(), await createTaskResponse.text()).toBe(200);
const createdTask = (await createTaskResponse.json()) as GenerationTaskResponse;
expect(createdTask.edit_plan_id || '').not.toBe('');
await expect(page.getByText(/任务状态:生成完成/)).toBeVisible({ timeout: 90_000 });
await expect(page.getByText(/生成失败|生成任务查询失败|生成结果查询失败/)).toHaveCount(0);
@@ -166,6 +172,7 @@ test.describe('Core generation and download flow', () => {
const reviewedVideoData = (await reviewedVideo.json()) as { review_status: string; generation_params: Record<string, unknown> };
expect(reviewedVideoData.review_status).toBe('approved');
expect(reviewedVideoData.generation_params.title_id).toBe(titleData.id);
expect(reviewedVideoData.generation_params.edit_plan_id).toBe(createdTask.edit_plan_id);
const downloadUrlResponse = await request.get(`${apiBase}/generated-videos/${generatedVideo.id}/download-url`, { headers });
expect(downloadUrlResponse.status(), await downloadUrlResponse.text()).toBe(200);
const downloadData = (await downloadUrlResponse.json()) as { download_url: string };
+49
View File
@@ -0,0 +1,49 @@
import apiClient from './client';
export interface EditTemplateItem {
id: string;
workspace_id: string;
project_id: string;
name: string;
description: string;
target_duration: number;
clip_count: number;
is_active: boolean;
}
export interface EditPlanClipItem {
id: string;
asset_id: string;
asset_name: string;
sequence: number;
start_time: number;
duration: number;
reason: string;
}
export interface EditPlanItem {
id: string;
workspace_id: string;
project_id: string;
template_id: string;
asset_library_id: string;
title_id: string;
status: string;
summary: string;
clips: EditPlanClipItem[];
}
export const getEditTemplates = async (projectId: string, workspaceId: string): Promise<EditTemplateItem[]> => {
const response = await apiClient.get(`/projects/${projectId}/edit-plans/templates`, { params: { workspace_id: workspaceId } });
return response.data;
};
export const createEditPlan = async ({ projectId, data }: { projectId: string; data: {
workspace_id: string;
asset_library_id: string;
template_id?: string;
title_id?: string;
} }): Promise<EditPlanItem> => {
const response = await apiClient.post(`/projects/${projectId}/edit-plans`, data);
return response.data;
};
+2
View File
@@ -7,6 +7,7 @@ export interface GenerationTaskItem {
asset_library_id: string;
strategy_id?: string | null;
voice_library_id?: string | null;
edit_plan_id?: string | null;
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
progress: number;
result_count: number;
@@ -37,6 +38,7 @@ export const createGenerationTask = async (data: {
asset_library_id: string;
strategy_id?: string;
voice_library_id?: string;
edit_plan_id?: string;
created_by_user_id?: string;
}): Promise<GenerationTaskItem> => {
const response = await apiClient.post('/generation/tasks', data);
@@ -1,11 +1,12 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { Alert, Button, Card, Form, Input, Select, Space, Typography, message } from 'antd';
import { Alert, Button, Card, Form, Input, List, Select, Space, Typography, message } from 'antd';
import { useMutation, useQuery } from '@tanstack/react-query';
import { createGenerationTask, getGeneratedVideoDownloadUrl, getGenerationResults, getGenerationTask } from '@/api/generation';
import { getAssetLibraries, getProjectAssetDiagnosis } from '@/api/assets';
import { getProject } from '@/api/projects';
import { getProjectTitles } from '@/api/projectTitles';
import { createEditPlan, getEditTemplates, EditPlanItem } from '@/api/editPlans';
import { useAuthStore } from '@/store/authStore';
const humanizeGenerationStatus = (status: string) => {
@@ -50,6 +51,7 @@ const ProjectGeneration: React.FC = () => {
const [resolvedWorkspaceId, setResolvedWorkspaceId] = useState<string>(routedWorkspaceId || storedWorkspaceId);
const workspaceId = routedWorkspaceId || storedWorkspaceId || resolvedWorkspaceId;
const [taskId, setTaskId] = useState('');
const [editPlan, setEditPlan] = useState<EditPlanItem | null>(null);
const [form] = Form.useForm();
useEffect(() => {
@@ -93,6 +95,12 @@ const ProjectGeneration: React.FC = () => {
enabled: !!projectId,
});
const templatesQuery = useQuery({
queryKey: ['edit-templates', projectId, workspaceId],
queryFn: () => getEditTemplates(projectId, workspaceId),
enabled: !!projectId && !!workspaceId,
});
const taskQuery = useQuery({
queryKey: ['generation-task', taskId],
queryFn: () => getGenerationTask(taskId),
@@ -118,6 +126,15 @@ const ProjectGeneration: React.FC = () => {
onError: (error: any) => message.error(humanizeGenerationError(error.response?.data?.detail || '创建生成任务失败')),
});
const editPlanMutation = useMutation({
mutationFn: createEditPlan,
onSuccess: (plan) => {
setEditPlan(plan);
message.success('剪辑计划已生成,请确认后再发起生成');
},
onError: (error: any) => message.error(error.response?.data?.detail || '生成剪辑计划失败'),
});
useEffect(() => {
if (taskQuery.data?.status === 'completed') {
resultsQuery.refetch();
@@ -139,6 +156,11 @@ const ProjectGeneration: React.FC = () => {
[titlesQuery.data]
);
const templateOptions = useMemo(
() => (templatesQuery.data || []).map((item) => ({ label: `${item.name} (${item.clip_count}段/${Math.round(item.target_duration)}秒)`, value: item.id })),
[templatesQuery.data]
);
const failedReason = taskQuery.data?.status === 'failed'
? humanizeGenerationError(taskQuery.data.error_message)
: '';
@@ -207,6 +229,7 @@ const ProjectGeneration: React.FC = () => {
asset_library_id: values.asset_library_id,
voice_library_id: values.voice_library_id || '',
strategy_id: values.title_id || values.strategy_id || '',
edit_plan_id: editPlan?.id || '',
created_by_user_id: user?.id || '',
})
}
@@ -220,14 +243,54 @@ const ProjectGeneration: React.FC = () => {
<Form.Item label="标题" name="title_id">
<Select allowClear options={titleOptions} placeholder="可选:选择标题库中的标题" loading={titlesQuery.isLoading} />
</Form.Item>
<Form.Item label="剪辑模板" name="template_id">
<Select allowClear options={templateOptions} placeholder="选择用于生成剪辑计划的模板" loading={templatesQuery.isLoading} />
</Form.Item>
<Form.Item label="策略 ID" name="strategy_id">
<Input placeholder="可选:例如 default-strategy" />
</Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={generationMutation.isPending} disabled={!canSubmitGeneration}></Button>
<Button
onClick={() => {
const values = form.getFieldsValue();
if (!values.asset_library_id) {
message.warning('请先选择素材库');
return;
}
editPlanMutation.mutate({
projectId,
data: {
workspace_id: workspaceId,
asset_library_id: values.asset_library_id,
template_id: values.template_id || '',
title_id: values.title_id || '',
},
});
}}
loading={editPlanMutation.isPending}
disabled={!canSubmitGeneration}
>
</Button>
<Button type="primary" htmlType="submit" loading={generationMutation.isPending} disabled={!canSubmitGeneration || !editPlan}></Button>
</Space>
</Form>
{editPlan && (
<Card size="small" style={{ marginTop: 16 }} title="剪辑计划预览">
<Typography.Paragraph>{editPlan.summary}</Typography.Paragraph>
<List
size="small"
dataSource={editPlan.clips}
renderItem={(clip) => (
<List.Item>
<Typography.Text>{clip.sequence}. {clip.asset_name} / {Math.round(clip.duration)} / {clip.reason}</Typography.Text>
</List.Item>
)}
/>
</Card>
)}
{taskQuery.data && (
<Alert
style={{ marginTop: 16 }}
@@ -245,6 +245,7 @@ def generate_video(task_id: str) -> dict:
"asset_library_id": task.asset_library_id,
"voice_library_id": task.voice_library_id,
"title_id": task.strategy_id,
"edit_plan_id": task.edit_plan_id,
"output_width": OUTPUT_WIDTH,
"output_height": OUTPUT_HEIGHT,
"output_fps": OUTPUT_FPS,
@@ -16,6 +16,7 @@ class SQLAlchemyGenerationTaskRepository:
strategy_id=task.strategy_id,
asset_library_id=task.asset_library_id,
voice_library_id=task.voice_library_id,
edit_plan_id=task.edit_plan_id,
status=task.status.value,
progress=task.progress,
result_count=task.result_count,
@@ -40,6 +41,7 @@ class SQLAlchemyGenerationTaskRepository:
strategy_id=model.strategy_id,
asset_library_id=model.asset_library_id,
voice_library_id=model.voice_library_id,
edit_plan_id=getattr(model, "edit_plan_id", "") or "",
status=GenerationTaskStatus(model.status),
progress=model.progress,
result_count=int(model.result_count or 0),
@@ -58,6 +60,8 @@ class SQLAlchemyGenerationTaskRepository:
model = self.session.query(GenerationTaskModel).filter(GenerationTaskModel.id == task.id).first()
if model is None:
raise ValueError(f"GenerationTask {task.id} not found")
model.voice_library_id = task.voice_library_id
model.edit_plan_id = task.edit_plan_id
model.status = task.status.value
model.progress = task.progress
model.result_count = task.result_count
@@ -131,6 +131,50 @@ class ProjectTitleModel(Base):
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditTemplateModel(Base):
__tablename__ = "edit_templates"
id = Column(String(32), primary_key=True)
workspace_id = Column(String(32), nullable=False, index=True)
project_id = Column(String(32), nullable=False, index=True)
name = Column(String(120), nullable=False)
description = Column(Text, nullable=False, default="")
target_duration = Column(Float, nullable=False, default=30)
clip_count = Column(Integer, nullable=False, default=3)
is_active = Column(Boolean, nullable=False, default=True)
created_by_user_id = Column(String(32), nullable=False, default="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditPlanModel(Base):
__tablename__ = "edit_plans"
id = Column(String(32), primary_key=True)
workspace_id = Column(String(32), nullable=False, index=True)
project_id = Column(String(32), nullable=False, index=True)
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="")
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="")
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
class EditPlanClipModel(Base):
__tablename__ = "edit_plan_clips"
id = Column(String(32), primary_key=True)
edit_plan_id = Column(String(32), nullable=False, index=True)
asset_id = Column(String(32), nullable=False, index=True)
sequence = Column(Integer, nullable=False)
start_time = Column(Float, nullable=False, default=0)
duration = Column(Float, nullable=False, default=0)
reason = Column(Text, nullable=False, default="")
class IngestJobModel(Base):
__tablename__ = "ingest_jobs"
@@ -170,6 +214,7 @@ class GenerationTaskModel(Base):
strategy_id = Column(String(32), nullable=False, default="")
asset_library_id = Column(String(32), nullable=False, index=True)
voice_library_id = Column(String(32), nullable=False, default="")
edit_plan_id = Column(String(32), nullable=False, default="", index=True)
status = Column(String(20), nullable=False, default="pending", index=True)
progress = Column(Float, nullable=False, default=0.0)
result_count = Column(Float, nullable=False, default=0)
+2
View File
@@ -13,6 +13,7 @@ class CreateGenerationTaskCommand:
asset_library_id: str
strategy_id: str = ""
voice_library_id: str = ""
edit_plan_id: str = ""
created_by_user_id: str = ""
@@ -27,6 +28,7 @@ class CreateGenerationTaskUseCase:
asset_library_id=command.asset_library_id,
strategy_id=command.strategy_id,
voice_library_id=command.voice_library_id,
edit_plan_id=command.edit_plan_id,
created_by_user_id=command.created_by_user_id,
)
return self.generation_task_repository.create(task)
+3
View File
@@ -22,6 +22,7 @@ class GenerationTask:
asset_library_id: str
strategy_id: str = ""
voice_library_id: str = ""
edit_plan_id: str = ""
status: GenerationTaskStatus = GenerationTaskStatus.PENDING
progress: float = 0.0
result_count: int = 0
@@ -40,6 +41,7 @@ class GenerationTask:
*,
strategy_id: str = "",
voice_library_id: str = "",
edit_plan_id: str = "",
created_by_user_id: str = "",
) -> "GenerationTask":
if not workspace_id.strip():
@@ -55,5 +57,6 @@ class GenerationTask:
asset_library_id=asset_library_id.strip(),
strategy_id=strategy_id.strip(),
voice_library_id=voice_library_id.strip(),
edit_plan_id=edit_plan_id.strip(),
created_by_user_id=created_by_user_id.strip(),
)