修复拆镜复刻路由,修补API参数说明,版本迁移异常修复
This commit is contained in:
+248
-108
@@ -8,122 +8,262 @@ from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '287c6c064c5d'
|
||||
down_revision: Union[str, None] = '9216bca75ccf'
|
||||
revision: str = "287c6c064c5d"
|
||||
down_revision: Union[str, None] = "9216bca75ccf"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _inspector():
|
||||
return inspect(op.get_bind())
|
||||
|
||||
|
||||
def _table_exists(table_name: str) -> bool:
|
||||
return table_name in _inspector().get_table_names(schema="public")
|
||||
|
||||
|
||||
def _column_exists(table_name: str, column_name: str) -> bool:
|
||||
if not _table_exists(table_name):
|
||||
return False
|
||||
columns = _inspector().get_columns(table_name, schema="public")
|
||||
return any(column["name"] == column_name for column in columns)
|
||||
|
||||
|
||||
def _index_exists(table_name: str, index_name: str) -> bool:
|
||||
if not _table_exists(table_name):
|
||||
return False
|
||||
indexes = _inspector().get_indexes(table_name, schema="public")
|
||||
return any(index["name"] == index_name for index in indexes)
|
||||
|
||||
|
||||
def _create_pre_test_template_table() -> None:
|
||||
op.create_table(
|
||||
"pre_test_template",
|
||||
sa.Column("id", sa.String(length=32), nullable=False, comment="主键"),
|
||||
sa.Column("name", sa.String(length=128), nullable=False, comment="模板名称"),
|
||||
sa.Column("user_id", sa.String(length=32), nullable=False, comment="用户id"),
|
||||
sa.Column("note", sa.Text(), nullable=True, comment="模板备注"),
|
||||
sa.Column("platform", sa.String(length=32), nullable=True, comment="投放平台(AD/QIANCHUAN/LOCAL)"),
|
||||
sa.Column("external_action", sa.String(length=64), nullable=True, comment="转化目标"),
|
||||
sa.Column("cpa_bid", sa.Float(), nullable=True, comment="目标转化成本:[1, 10000]"),
|
||||
sa.Column("audience_gender", sa.String(length=16), nullable=True, comment="性别(ALL/MALE/FEMALE)"),
|
||||
sa.Column("audience_age", sa.Text(), nullable=True, comment="受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]"),
|
||||
sa.Column("audience_region", sa.Text(), nullable=True, comment="受众地区,JSON数组(二级行政区域code)"),
|
||||
sa.Column("audience_network", sa.Text(), nullable=True, comment="网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]"),
|
||||
sa.Column("cus_name", sa.String(length=256), nullable=True, comment="客户主体名称"),
|
||||
sa.Column("pricing_type", sa.String(length=16), nullable=True, comment="出价类型(OCPC/CPA/OCPM)"),
|
||||
sa.Column("cost_cap", sa.Boolean(), nullable=True, comment="是否最优成本出价(仅AD支持)"),
|
||||
sa.Column("target_cost", sa.Boolean(), nullable=True, comment="是否稳定成本出价(仅AD支持)"),
|
||||
sa.Column("nobid", sa.Boolean(), nullable=True, comment="是否最大转化出价(仅AD支持)"),
|
||||
sa.Column("cpc_bid", sa.Float(), nullable=True, comment="目标点击成本:[1, 10000]"),
|
||||
sa.Column("budget", sa.Float(), nullable=True, comment="预算金额:[1, 10000]"),
|
||||
sa.Column("is_default", sa.Boolean(), nullable=True, comment="是否默认模板"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id", name="pre_test_template_pkey"),
|
||||
)
|
||||
|
||||
if not _index_exists("pre_test_template", "ix_pre_test_template_user_id"):
|
||||
op.create_index(
|
||||
"ix_pre_test_template_user_id",
|
||||
"pre_test_template",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def _upgrade_existing_pre_test_template_table() -> None:
|
||||
if not _column_exists("pre_test_template", "note"):
|
||||
op.add_column(
|
||||
"pre_test_template",
|
||||
sa.Column("note", sa.Text(), nullable=True, comment="模板备注"),
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "platform"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"platform",
|
||||
existing_type=sa.VARCHAR(length=32),
|
||||
nullable=True,
|
||||
existing_comment="投放平台(AD/QIANCHUAN/LOCAL)",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "external_action"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"external_action",
|
||||
existing_type=sa.VARCHAR(length=64),
|
||||
nullable=True,
|
||||
existing_comment="转化目标",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "audience_gender"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"audience_gender",
|
||||
existing_type=sa.VARCHAR(length=16),
|
||||
nullable=True,
|
||||
existing_comment="性别(ALL/MALE/FEMALE)",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "audience_age"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"audience_age",
|
||||
existing_type=sa.TEXT(),
|
||||
comment="受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]",
|
||||
existing_comment="受众年龄,JSON数组",
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "audience_network"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"audience_network",
|
||||
existing_type=sa.TEXT(),
|
||||
comment="网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]",
|
||||
existing_comment="网络类型,JSON数组",
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "pricing_type"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"pricing_type",
|
||||
existing_type=sa.VARCHAR(length=16),
|
||||
nullable=True,
|
||||
existing_comment="出价类型(OCPC/CPA/OCPM)",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "cost_cap"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"cost_cap",
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment="是否最优成本出价(仅AD支持)",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "target_cost"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"target_cost",
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment="是否稳定成本出价(仅AD支持)",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "nobid"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"nobid",
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment="是否最大转化出价(仅AD支持)",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "is_default"):
|
||||
op.alter_column(
|
||||
"pre_test_template",
|
||||
"is_default",
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment="是否默认模板",
|
||||
)
|
||||
|
||||
if _column_exists("pre_test_template", "status"):
|
||||
op.drop_column("pre_test_template", "status")
|
||||
|
||||
if _column_exists("pre_test_template", "description"):
|
||||
op.drop_column("pre_test_template", "description")
|
||||
|
||||
if not _index_exists("pre_test_template", "ix_pre_test_template_user_id"):
|
||||
op.create_index(
|
||||
"ix_pre_test_template_user_id",
|
||||
"pre_test_template",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('pre_test_template', sa.Column('note', sa.Text(), nullable=True, comment='模板备注'))
|
||||
op.alter_column('pre_test_template', 'platform',
|
||||
existing_type=sa.VARCHAR(length=32),
|
||||
nullable=True,
|
||||
existing_comment='投放平台(AD/QIANCHUAN/LOCAL)')
|
||||
op.alter_column('pre_test_template', 'external_action',
|
||||
existing_type=sa.VARCHAR(length=64),
|
||||
nullable=True,
|
||||
existing_comment='转化目标')
|
||||
op.alter_column('pre_test_template', 'audience_gender',
|
||||
existing_type=sa.VARCHAR(length=16),
|
||||
nullable=True,
|
||||
existing_comment='性别(ALL/MALE/FEMALE)')
|
||||
op.alter_column('pre_test_template', 'audience_age',
|
||||
existing_type=sa.TEXT(),
|
||||
comment='受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]',
|
||||
existing_comment='受众年龄,JSON数组',
|
||||
existing_nullable=True)
|
||||
op.alter_column('pre_test_template', 'audience_network',
|
||||
existing_type=sa.TEXT(),
|
||||
comment='网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]',
|
||||
existing_comment='网络类型,JSON数组',
|
||||
existing_nullable=True)
|
||||
op.alter_column('pre_test_template', 'pricing_type',
|
||||
existing_type=sa.VARCHAR(length=16),
|
||||
nullable=True,
|
||||
existing_comment='出价类型(OCPC/CPA/OCPM)')
|
||||
op.alter_column('pre_test_template', 'cost_cap',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment='是否最优成本出价(仅AD支持)')
|
||||
op.alter_column('pre_test_template', 'target_cost',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment='是否稳定成本出价(仅AD支持)')
|
||||
op.alter_column('pre_test_template', 'nobid',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment='是否最大转化出价(仅AD支持)')
|
||||
op.alter_column('pre_test_template', 'is_default',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=True,
|
||||
existing_comment='是否默认模板')
|
||||
op.drop_column('pre_test_template', 'status')
|
||||
op.drop_column('pre_test_template', 'description')
|
||||
op.add_column('resources_material', sa.Column('task_id', sa.String(length=32), nullable=True, comment='前测任务id'))
|
||||
op.add_column('resources_material', sa.Column('note', sa.Text(), nullable=True, comment='前测失败备注或者其他备注'))
|
||||
op.add_column('resources_material', sa.Column('status', sa.String(length=16), nullable=True, comment='前测状态(FAILED/PENDING/SUCCESS)'))
|
||||
op.add_column('resources_material', sa.Column('pre_result', sa.Text(), nullable=True, comment='前测结果,JSON数组对象'))
|
||||
op.add_column('resources_material', sa.Column('pre_test_template_id', sa.String(length=32), nullable=True, comment='前测模板id'))
|
||||
op.create_index(op.f('ix_resources_material_task_id'), 'resources_material', ['task_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
# pre_test_template 是这版迁移缺失的核心表。
|
||||
# 如果不存在,直接按当前模型源码创建完整表。
|
||||
# 如果已存在,则按原迁移逻辑补 note、调整 nullable/comment、删除旧字段。
|
||||
if not _table_exists("pre_test_template"):
|
||||
_create_pre_test_template_table()
|
||||
else:
|
||||
_upgrade_existing_pre_test_template_table()
|
||||
|
||||
# resources_material 追加字段,按源码迁移逻辑保留,同时增加字段存在判断,避免重复执行报错。
|
||||
if _table_exists("resources_material"):
|
||||
if not _column_exists("resources_material", "task_id"):
|
||||
op.add_column(
|
||||
"resources_material",
|
||||
sa.Column("task_id", sa.String(length=32), nullable=True, comment="前测任务id"),
|
||||
)
|
||||
|
||||
if not _column_exists("resources_material", "note"):
|
||||
op.add_column(
|
||||
"resources_material",
|
||||
sa.Column("note", sa.Text(), nullable=True, comment="前测失败备注或者其他备注"),
|
||||
)
|
||||
|
||||
if not _column_exists("resources_material", "status"):
|
||||
op.add_column(
|
||||
"resources_material",
|
||||
sa.Column("status", sa.String(length=16), nullable=True, comment="前测状态(FAILED/PENDING/SUCCESS)"),
|
||||
)
|
||||
|
||||
if not _column_exists("resources_material", "pre_result"):
|
||||
op.add_column(
|
||||
"resources_material",
|
||||
sa.Column("pre_result", sa.Text(), nullable=True, comment="前测结果,JSON数组对象"),
|
||||
)
|
||||
|
||||
if not _column_exists("resources_material", "pre_test_template_id"):
|
||||
op.add_column(
|
||||
"resources_material",
|
||||
sa.Column("pre_test_template_id", sa.String(length=32), nullable=True, comment="前测模板id"),
|
||||
)
|
||||
|
||||
if not _index_exists("resources_material", "ix_resources_material_task_id"):
|
||||
op.create_index(
|
||||
"ix_resources_material_task_id",
|
||||
"resources_material",
|
||||
["task_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_resources_material_task_id'), table_name='resources_material')
|
||||
op.drop_column('resources_material', 'pre_test_template_id')
|
||||
op.drop_column('resources_material', 'pre_result')
|
||||
op.drop_column('resources_material', 'status')
|
||||
op.drop_column('resources_material', 'note')
|
||||
op.drop_column('resources_material', 'task_id')
|
||||
op.add_column('pre_test_template', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True, comment='模板描述'))
|
||||
op.add_column('pre_test_template', sa.Column('status', sa.INTEGER(), autoincrement=False, nullable=False, comment='状态(0=禁用,1=启用)'))
|
||||
op.alter_column('pre_test_template', 'is_default',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_comment='是否默认模板')
|
||||
op.alter_column('pre_test_template', 'nobid',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_comment='是否最大转化出价(仅AD支持)')
|
||||
op.alter_column('pre_test_template', 'target_cost',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_comment='是否稳定成本出价(仅AD支持)')
|
||||
op.alter_column('pre_test_template', 'cost_cap',
|
||||
existing_type=sa.BOOLEAN(),
|
||||
nullable=False,
|
||||
existing_comment='是否最优成本出价(仅AD支持)')
|
||||
op.alter_column('pre_test_template', 'pricing_type',
|
||||
existing_type=sa.VARCHAR(length=16),
|
||||
nullable=False,
|
||||
existing_comment='出价类型(OCPC/CPA/OCPM)')
|
||||
op.alter_column('pre_test_template', 'audience_network',
|
||||
existing_type=sa.TEXT(),
|
||||
comment='网络类型,JSON数组',
|
||||
existing_comment='网络类型,JSON数组, 格式:[ALL,5G,4G,3G,2G,WIFI]',
|
||||
existing_nullable=True)
|
||||
op.alter_column('pre_test_template', 'audience_age',
|
||||
existing_type=sa.TEXT(),
|
||||
comment='受众年龄,JSON数组',
|
||||
existing_comment='受众年龄,JSON数组, 格式:[ALL,18-23, 24-30, 31-40, 41-49, 50+]',
|
||||
existing_nullable=True)
|
||||
op.alter_column('pre_test_template', 'audience_gender',
|
||||
existing_type=sa.VARCHAR(length=16),
|
||||
nullable=False,
|
||||
existing_comment='性别(ALL/MALE/FEMALE)')
|
||||
op.alter_column('pre_test_template', 'external_action',
|
||||
existing_type=sa.VARCHAR(length=64),
|
||||
nullable=False,
|
||||
existing_comment='转化目标')
|
||||
op.alter_column('pre_test_template', 'platform',
|
||||
existing_type=sa.VARCHAR(length=32),
|
||||
nullable=False,
|
||||
existing_comment='投放平台(AD/QIANCHUAN/LOCAL)')
|
||||
op.drop_column('pre_test_template', 'note')
|
||||
# ### end Alembic commands ###
|
||||
if _table_exists("resources_material"):
|
||||
if _index_exists("resources_material", "ix_resources_material_task_id"):
|
||||
op.drop_index("ix_resources_material_task_id", table_name="resources_material")
|
||||
|
||||
if _column_exists("resources_material", "pre_test_template_id"):
|
||||
op.drop_column("resources_material", "pre_test_template_id")
|
||||
|
||||
if _column_exists("resources_material", "pre_result"):
|
||||
op.drop_column("resources_material", "pre_result")
|
||||
|
||||
if _column_exists("resources_material", "status"):
|
||||
op.drop_column("resources_material", "status")
|
||||
|
||||
if _column_exists("resources_material", "note"):
|
||||
op.drop_column("resources_material", "note")
|
||||
|
||||
if _column_exists("resources_material", "task_id"):
|
||||
op.drop_column("resources_material", "task_id")
|
||||
|
||||
# 这一版迁移负责新增 pre_test_template 表,所以回滚时删除该表。
|
||||
if _table_exists("pre_test_template"):
|
||||
if _index_exists("pre_test_template", "ix_pre_test_template_user_id"):
|
||||
op.drop_index("ix_pre_test_template_user_id", table_name="pre_test_template")
|
||||
|
||||
op.drop_table("pre_test_template")
|
||||
@@ -8,6 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_current_user, get_db
|
||||
from app.models.user import User
|
||||
from app.enums.common import ModuleProjectStatusEnum
|
||||
from app.enums.hot_opening_replicate import HotOpeningStepCodeEnum, ModuleCodeEnum
|
||||
from app.schemas.hot_opening_replicate import (
|
||||
HotOpeningActionOut,
|
||||
@@ -195,7 +196,7 @@ async def _mark_dispatch_failed_and_raise(
|
||||
"/spec",
|
||||
response_model=HotOpeningSpecOut,
|
||||
summary="查询爆款开头复刻模块状态枚举和步骤 JSON 结构说明",
|
||||
description="返回总任务状态、子任务状态、5个固定步骤编码以及每个步骤 input_json/output_json 的统一结构示例,方便前端和排查人员对照。",
|
||||
description="保留给调试和前端兜底读取。原业务接口已经在 Path、Query、Body 和响应模型字段上直接展示参数说明与枚举值。",
|
||||
)
|
||||
async def get_spec():
|
||||
return HotOpeningSpecOut()
|
||||
@@ -239,13 +240,13 @@ async def create_task(
|
||||
description="分页查询爆款开头复刻总任务项目列表。普通用户只能查看自己的项目,管理员可查看全部。",
|
||||
)
|
||||
async def list_tasks(
|
||||
status: str | None = Query(None, description="总任务状态筛选,例如 waiting_user、processing、completed、failed;为空不过滤"),
|
||||
status: ModuleProjectStatusEnum | None = Query(None, description="总任务状态筛选:pending=已创建,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消;为空不过滤"),
|
||||
page: int = Query(1, ge=1, description="分页页码,从1开始"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量,范围1-100"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await list_hot_opening_projects(db, current_user=current_user, status=status, page=page, page_size=page_size)
|
||||
return await list_hot_opening_projects(db, current_user=current_user, status=status.value if status else None, page=page, page_size=page_size)
|
||||
|
||||
|
||||
@router.get(
|
||||
|
||||
@@ -8,7 +8,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_current_user, get_db
|
||||
from app.models.user import User
|
||||
from app.enums.shot_replicate import ModuleCodeEnum, ShotReplicateStepCodeEnum
|
||||
from app.enums.shot_replicate import (
|
||||
ModuleCodeEnum,
|
||||
ShotAnalysisStatusEnum,
|
||||
ShotReplicateStepCodeEnum,
|
||||
ShotSegmentAnalysisStatusEnum,
|
||||
ShotSegmentReplicateStatusEnum,
|
||||
ShotSegmentSourceModeEnum,
|
||||
ShotSplitStatusEnum,
|
||||
ShotTaskSetStatusEnum,
|
||||
)
|
||||
from app.schemas.shot_replicate import (
|
||||
ShotReplicateActionOut,
|
||||
ShotReplicateDeleteOut,
|
||||
@@ -222,6 +231,7 @@ async def _mark_dispatch_failed_and_raise(
|
||||
"/spec",
|
||||
response_model=ShotReplicateSpecOut,
|
||||
summary="查询拆镜复刻模块状态枚举和步骤 JSON 结构说明",
|
||||
description="保留给调试和前端兜底读取。原业务接口已经在 Path、Query、Body 和响应模型字段上直接展示参数说明与枚举值。",
|
||||
)
|
||||
async def get_spec():
|
||||
return ShotReplicateSpecOut()
|
||||
@@ -231,9 +241,14 @@ async def get_spec():
|
||||
"/task-sets",
|
||||
response_model=ShotTaskSetDetailOut,
|
||||
summary="创建拆镜总任务集并异步分析原视频",
|
||||
description=(
|
||||
"创建拆镜总任务集,保存原视频地址和时长,随后异步投递原视频 AI 分析任务。"
|
||||
"分析完成后会写入原视频内容、分类、受众和 AI 建议拆镜时间段。"
|
||||
"状态枚举直接见本接口响应字段:status、analysis_status、split_status。"
|
||||
),
|
||||
)
|
||||
async def create_shot_task_set(
|
||||
req: ShotTaskSetCreate = Body(...),
|
||||
req: ShotTaskSetCreate = Body(..., description="创建拆镜总任务集参数:原视频 URL、视频时长、标题和可选幂等键"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -275,21 +290,21 @@ async def create_shot_task_set(
|
||||
summary="查询拆镜总任务集列表",
|
||||
)
|
||||
async def list_shot_task_sets(
|
||||
status: str | None = Query(None, description="总任务状态,见 ShotTaskSetStatusEnum"),
|
||||
analysis_status: str | None = Query(None, description="分析状态,见 ShotAnalysisStatusEnum"),
|
||||
split_status: str | None = Query(None, description="拆镜状态,见 ShotSplitStatusEnum"),
|
||||
keyword: str | None = Query(None, description="标题/内容关键词"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
status: ShotTaskSetStatusEnum | None = Query(None, description="总任务状态筛选:pending_analysis=等待分析,analyzing=分析中,analysis_completed=分析完成,analysis_failed=分析失败,splitting=拆镜中,split_completed=拆镜完成,partial_failed=部分失败,failed=失败,deleted=已软删"),
|
||||
analysis_status: ShotAnalysisStatusEnum | None = Query(None, description="原视频分析状态筛选:pending=待分析,processing=分析中,completed=分析完成,failed=分析失败"),
|
||||
split_status: ShotSplitStatusEnum | None = Query(None, description="拆镜状态筛选:none=尚未拆镜,pending=待拆镜,processing=拆镜中,completed=拆镜完成,failed=拆镜失败,retry_waiting=等待恢复重试"),
|
||||
keyword: str | None = Query(None, description="标题/原视频内容/分类/受众关键词,模糊搜索;为空不过滤"),
|
||||
page: int = Query(1, ge=1, description="分页页码,从1开始"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量,范围1-100"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
return await list_task_sets(
|
||||
db,
|
||||
current_user=current_user,
|
||||
status=status,
|
||||
analysis_status=analysis_status,
|
||||
split_status=split_status,
|
||||
status=status.value if status else None,
|
||||
analysis_status=analysis_status.value if analysis_status else None,
|
||||
split_status=split_status.value if split_status else None,
|
||||
keyword=keyword,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
@@ -300,9 +315,10 @@ async def list_shot_task_sets(
|
||||
"/task-sets/{task_set_id}",
|
||||
response_model=ShotTaskSetDetailOut,
|
||||
summary="获取拆镜总任务集详情",
|
||||
description="根据拆镜总任务集ID查询详情,包含原视频分析结果、AI 建议拆镜列表和当前总任务状态。",
|
||||
)
|
||||
async def get_shot_task_set(
|
||||
task_set_id: str = Path(...),
|
||||
task_set_id: str = Path(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -313,10 +329,14 @@ async def get_shot_task_set(
|
||||
"/task-sets/{task_set_id}/split-by-ai",
|
||||
response_model=ShotSplitByAIOut,
|
||||
summary="按 AI 建议方案异步拆镜",
|
||||
description=(
|
||||
"基于原视频 AI 分析生成的建议时间段创建拆镜片段,并异步投递 ffmpeg 切割任务。"
|
||||
"selected_indices 不传时默认按全部 AI 建议拆镜;replace_existing=true 时会软删旧 AI 建议片段后重新创建。"
|
||||
),
|
||||
)
|
||||
async def split_by_ai(
|
||||
task_set_id: str = Path(...),
|
||||
req: ShotSplitByAIRequest = Body(default_factory=ShotSplitByAIRequest),
|
||||
task_set_id: str = Path(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id"),
|
||||
req: ShotSplitByAIRequest = Body(default_factory=ShotSplitByAIRequest, description="AI 建议拆镜参数:可选择建议序号,也可选择是否覆盖旧 AI 片段"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -345,10 +365,11 @@ async def split_by_ai(
|
||||
"/task-sets/{task_set_id}/split-custom",
|
||||
response_model=ShotSplitCustomOut,
|
||||
summary="按用户自定义开始/结束秒异步拆单条片段",
|
||||
description="按用户传入的 start_second/end_second 创建 custom 来源片段,并异步投递 ffmpeg 切割任务;切割完成后可进入复刻项目。",
|
||||
)
|
||||
async def split_custom(
|
||||
task_set_id: str = Path(...),
|
||||
req: ShotSplitCustomRequest = Body(...),
|
||||
task_set_id: str = Path(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id"),
|
||||
req: ShotSplitCustomRequest = Body(..., description="自定义拆镜时间段参数,end_second 必须大于 start_second"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -376,15 +397,16 @@ async def split_custom(
|
||||
"/task-sets/{task_set_id}/segments",
|
||||
response_model=ShotSegmentListOut,
|
||||
summary="查询拆镜片段列表",
|
||||
description="分页查询指定拆镜总任务集下的片段列表,可按来源、切割状态、片段分析状态和复刻状态筛选。",
|
||||
)
|
||||
async def list_task_set_segments(
|
||||
task_set_id: str = Path(...),
|
||||
source_mode: str | None = Query(None, description="ai_suggestion/custom"),
|
||||
split_status: str | None = Query(None, description="拆镜状态"),
|
||||
analysis_status: str | None = Query(None, description="片段分析状态"),
|
||||
replicate_status: str | None = Query(None, description="复刻状态"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
task_set_id: str = Path(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id"),
|
||||
source_mode: ShotSegmentSourceModeEnum | None = Query(None, description="片段来源:ai_suggestion=AI 建议拆镜,custom=用户自定义拆镜"),
|
||||
split_status: ShotSplitStatusEnum | None = Query(None, description="切割状态:none=尚未拆镜,pending=待拆镜,processing=拆镜中,completed=拆镜完成,failed=拆镜失败,retry_waiting=等待恢复重试"),
|
||||
analysis_status: ShotSegmentAnalysisStatusEnum | None = Query(None, description="片段分析状态:not_required=无需单独分析,pending=等待分析,processing=分析中,completed=分析完成,failed=分析失败"),
|
||||
replicate_status: ShotSegmentReplicateStatusEnum | None = Query(None, description="片段复刻状态:not_started=未复刻,project_created=已创建复刻项目,processing=复刻处理中,completed=复刻完成,failed=复刻失败"),
|
||||
page: int = Query(1, ge=1, description="分页页码,从1开始"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="每页数量,范围1-100"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -392,10 +414,10 @@ async def list_task_set_segments(
|
||||
db,
|
||||
current_user=current_user,
|
||||
task_set_id=task_set_id,
|
||||
source_mode=source_mode,
|
||||
split_status=split_status,
|
||||
analysis_status=analysis_status,
|
||||
replicate_status=replicate_status,
|
||||
source_mode=source_mode.value if source_mode else None,
|
||||
split_status=split_status.value if split_status else None,
|
||||
analysis_status=analysis_status.value if analysis_status else None,
|
||||
replicate_status=replicate_status.value if replicate_status else None,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
@@ -405,9 +427,10 @@ async def list_task_set_segments(
|
||||
"/segments/{segment_id}",
|
||||
response_model=ShotSegmentDetailOut,
|
||||
summary="获取拆镜片段详情",
|
||||
description="根据拆镜片段ID查询单个片段详情,包含切割结果、片段分析结果、复刻项目ID和状态。",
|
||||
)
|
||||
async def get_segment(
|
||||
segment_id: str = Path(...),
|
||||
segment_id: str = Path(..., description="拆镜片段ID,即 shot_replicate_segments.id"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -418,10 +441,15 @@ async def get_segment(
|
||||
"/segments/{segment_id}/replication-projects",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="将拆镜片段创建为拆镜复刻项目",
|
||||
description=(
|
||||
"以拆镜片段作为锁定素材视频创建 ModuleGenerationProject 复刻项目。"
|
||||
"创建后只同步生成第1步 material_input,后续第2-5步需要调用 /projects/{project_id}/steps/{step_id}/... 系列接口手动推进。"
|
||||
"素材视频来自片段 segment_video_url,不允许前端传入或后续修改。"
|
||||
),
|
||||
)
|
||||
async def create_replication_project_from_segment(
|
||||
segment_id: str = Path(...),
|
||||
req: ShotSegmentReplicationCreateRequest = Body(...),
|
||||
segment_id: str = Path(..., description="拆镜片段ID,即 shot_replicate_segments.id"),
|
||||
req: ShotSegmentReplicationCreateRequest = Body(..., description="从拆镜片段创建复刻项目参数:生成项目名称、核心内容点、新产品图片和可选幂等键"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -450,9 +478,10 @@ async def create_replication_project_from_segment(
|
||||
"/projects/{project_id}",
|
||||
response_model=ShotReplicateTaskDetailOut,
|
||||
summary="获取拆镜复刻项目详情",
|
||||
description="获取拆镜复刻 ModuleGenerationProject 项目详情,聚合返回素材、图片提词、图片生成、视频提词、视频生成和当前有效步骤列表。",
|
||||
)
|
||||
async def get_project(
|
||||
project_id: str = Path(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -463,10 +492,15 @@ async def get_project(
|
||||
"/projects/{project_id}/material",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="修改拆镜复刻素材信息,素材视频不允许修改",
|
||||
description=(
|
||||
"修改拆镜复刻第1步素材输入,并重建第1步 material_input 新版本。"
|
||||
"素材视频 material_video_url 锁定为拆镜片段视频,不允许修改;可修改新产品图片、参考素材项目名、生成项目名和核心内容点。"
|
||||
"修改后会软删除第2、3、4、5步当前有效任务,并清空旧图片/视频结果。"
|
||||
),
|
||||
)
|
||||
async def update_material(
|
||||
project_id: str = Path(...),
|
||||
req: ShotReplicateMaterialUpdateRequest = Body(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
req: ShotReplicateMaterialUpdateRequest = Body(..., description="第1步素材输入修改参数;不接收 material_video_url,至少传一个允许修改字段"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -486,12 +520,13 @@ async def update_material(
|
||||
@router.put(
|
||||
"/projects/{project_id}/steps/{step_id}/image-prompt",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="直接修改图片 AI 优化提词",
|
||||
summary="直接修改第2步图片 AI 优化提词",
|
||||
description="直接修改第2步 image_prompt_optimize 的图片提示词,不调用 AI、不扣积分;保存后软删除第3、4、5步当前有效任务。",
|
||||
)
|
||||
async def update_image_prompt(
|
||||
project_id: str = Path(...),
|
||||
step_id: str = Path(...),
|
||||
req: ShotReplicateImagePromptUpdateRequest = Body(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
step_id: str = Path(..., description="第2步图片 AI 提词子任务ID,即 module_generation_steps.id,step_code=image_prompt_optimize"),
|
||||
req: ShotReplicateImagePromptUpdateRequest = Body(..., description="图片 AI 优化提词修改参数"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -512,12 +547,16 @@ async def update_image_prompt(
|
||||
@router.put(
|
||||
"/projects/{project_id}/steps/{step_id}/video-prompt-schema",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="修改视频 AI 提词 JSON schema",
|
||||
summary="修改第4步视频 AI 提词 JSON schema",
|
||||
description=(
|
||||
"修改第4步 video_prompt_optimize 的视频提词 JSON schema,不调用 AI、不扣积分。"
|
||||
"服务端会锁定视频时长、比例、分辨率、帧率、动态时间规划等关键结构;保存后软删除第5步视频生成任务。"
|
||||
),
|
||||
)
|
||||
async def update_video_prompt_schema(
|
||||
project_id: str = Path(...),
|
||||
step_id: str = Path(...),
|
||||
req: ShotReplicateVideoPromptSchemaUpdateRequest = Body(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
step_id: str = Path(..., description="第4步视频 AI 提词子任务ID,即 module_generation_steps.id,step_code=video_prompt_optimize"),
|
||||
req: ShotReplicateVideoPromptSchemaUpdateRequest = Body(..., description="视频 AI 提词 JSON schema 修改参数"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
@@ -536,19 +575,24 @@ async def update_video_prompt_schema(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/generate-image-prompt",
|
||||
"/projects/{project_id}/steps/{step_id}/generate-image-prompt",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="生成图片 AI 提词",
|
||||
summary="基于第1步素材输入生成图片 AI 提词",
|
||||
description=(
|
||||
"基于第1步 material_input 子任务手动生成第2步 image_prompt_optimize。"
|
||||
"如果已存在旧的第2、3、4、5步,会先软删除旧步骤,再创建新的第2步并投递 Celery 文本提词任务。"
|
||||
),
|
||||
)
|
||||
async def generate_image_prompt(
|
||||
project_id: str = Path(...),
|
||||
req: ShotReplicateGenerateImagePromptRequest = Body(default_factory=ShotReplicateGenerateImagePromptRequest),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
step_id: str = Path(..., description="第1步素材输入子任务ID,即 module_generation_steps.id,step_code=material_input"),
|
||||
req: ShotReplicateGenerateImagePromptRequest = Body(default_factory=ShotReplicateGenerateImagePromptRequest, description="图片提词生成参数,当前无需传参,额外字段会忽略"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
|
||||
try:
|
||||
project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req)
|
||||
project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, material_step_id=step_id, req=req)
|
||||
project_id_value, step_id_value = project.id, step.id
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
@@ -577,19 +621,25 @@ async def generate_image_prompt(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/generate-image",
|
||||
"/projects/{project_id}/steps/{step_id}/generate-image",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="根据图片 AI 提词生成图片",
|
||||
summary="基于第2步图片 AI 提词生成图片",
|
||||
description=(
|
||||
"基于第2步 image_prompt_optimize 子任务生成第3步 image_generate。"
|
||||
"请求体传入图片引擎和图片参数;ChatGenerationTask 幂等键由后端自动生成。"
|
||||
"如果已存在旧的第3、4、5步,会先软删除旧步骤,再创建新的第3步。"
|
||||
),
|
||||
)
|
||||
async def generate_image(
|
||||
project_id: str = Path(...),
|
||||
req: ShotReplicateGenerateImageRequest = Body(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
step_id: str = Path(..., description="第2步图片 AI 提词子任务ID,即 module_generation_steps.id,step_code=image_prompt_optimize"),
|
||||
req: ShotReplicateGenerateImageRequest = Body(..., description="图片生成引擎和参数;可选值来自图片引擎配置接口"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
|
||||
try:
|
||||
project, step, chat_task = await generate_image_from_prompt(db, current_user=current_user, project_id=project_id, req=req)
|
||||
project, step, chat_task = await generate_image_from_prompt(db, current_user=current_user, project_id=project_id, prompt_step_id=step_id, req=req)
|
||||
project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
@@ -611,19 +661,25 @@ async def generate_image(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/generate-video-prompt",
|
||||
"/projects/{project_id}/steps/{step_id}/generate-video-prompt",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="生成视频 AI 提词 JSON schema",
|
||||
summary="基于第3步图片结果生成视频 AI 提词 JSON schema",
|
||||
description=(
|
||||
"基于第3步 image_generate 子任务生成第4步 video_prompt_optimize。"
|
||||
"视频时长、比例、分辨率在本步骤确定并写入第4步 output_json;第5步视频生成只选择视频引擎。"
|
||||
"如果已存在旧的第4、5步,会先软删除旧步骤,再创建新的第4步并投递 Celery 文本提词任务。"
|
||||
),
|
||||
)
|
||||
async def generate_video_prompt(
|
||||
project_id: str = Path(...),
|
||||
req: ShotReplicateGenerateVideoPromptRequest = Body(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
step_id: str = Path(..., description="第3步图片生成子任务ID,即 module_generation_steps.id,step_code=image_generate"),
|
||||
req: ShotReplicateGenerateVideoPromptRequest = Body(..., description="视频提词生成参数:视频引擎、时长、比例、分辨率和目标平台;可选值来自视频引擎配置接口"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
|
||||
try:
|
||||
project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req)
|
||||
project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, image_step_id=step_id, req=req)
|
||||
project_id_value, step_id_value = project.id, step.id
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
@@ -652,19 +708,25 @@ async def generate_video_prompt(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/generate-video",
|
||||
"/projects/{project_id}/steps/{step_id}/generate-video",
|
||||
response_model=ShotReplicateActionOut,
|
||||
summary="根据视频 AI 提词生成视频",
|
||||
summary="基于第4步视频 AI 提词生成最终视频",
|
||||
description=(
|
||||
"基于第4步 video_prompt_optimize 子任务生成第5步 video_generate。"
|
||||
"请求体只需要选择视频生成引擎 engine_id;duration、aspect_ratio、resolution 从第4步视频提词结果继承。"
|
||||
"如果已存在旧的第5步,会先软删除旧步骤,再创建新的第5步。"
|
||||
),
|
||||
)
|
||||
async def generate_video(
|
||||
project_id: str = Path(...),
|
||||
req: ShotReplicateGenerateVideoRequest = Body(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
step_id: str = Path(..., description="第4步视频 AI 提词子任务ID,即 module_generation_steps.id,step_code=video_prompt_optimize"),
|
||||
req: ShotReplicateGenerateVideoRequest = Body(..., description="视频生成参数:只传 engine_id,其它视频参数继承第4步视频提词结果"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
||||
_ensure_celery_enabled(current_user=current_user, project_id=project_id, step_id=step_id)
|
||||
try:
|
||||
project, step, chat_task = await generate_video_from_prompt(db, current_user=current_user, project_id=project_id, req=req)
|
||||
project, step, chat_task = await generate_video_from_prompt(db, current_user=current_user, project_id=project_id, prompt_step_id=step_id, req=req)
|
||||
project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
|
||||
await db.commit()
|
||||
except HTTPException:
|
||||
@@ -689,9 +751,10 @@ async def generate_video(
|
||||
"/projects/{project_id}",
|
||||
response_model=ShotReplicateDeleteOut,
|
||||
summary="软删除拆镜复刻项目",
|
||||
description="软删除拆镜复刻 ModuleGenerationProject,并联动软删除当前有效步骤和关联的 ChatGenerationTask。",
|
||||
)
|
||||
async def delete_project(
|
||||
project_id: str = Path(...),
|
||||
project_id: str = Path(..., description="拆镜复刻项目ID,即 module_generation_projects.id"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
|
||||
@@ -329,9 +329,9 @@ class HotOpeningGenerateImageRequest(BaseModel):
|
||||
)
|
||||
|
||||
engine_id: str | None = Field(None, description="图片生成引擎ID。为空则使用当前启用且优先级最高的图片引擎")
|
||||
image_size: str | None = Field(None, description="图片分辨率档位,例如 1K、2K。为空使用引擎默认值")
|
||||
image_proportion: str | None = Field(None, description="图片比例,例如 1:1、16:9、9:16。为空使用默认值")
|
||||
image_px: str | None = Field(None, description="图片像素尺寸,例如 2048x2048。为空时按引擎支持尺寸自动匹配")
|
||||
image_size: str | None = Field(None, description="图片分辨率档位,例如 1K、2K。具体可选值来自图片引擎配置接口;为空使用引擎默认值")
|
||||
image_proportion: str | None = Field(None, description="图片比例,例如 1:1、16:9、9:16。具体可选值来自图片引擎配置接口;为空使用默认值")
|
||||
image_px: str | None = Field(None, description="图片像素尺寸,例如 1024x1024、2048x2048。具体可选值来自图片引擎配置接口;为空时按引擎支持尺寸自动匹配")
|
||||
|
||||
|
||||
class HotOpeningGenerateVideoPromptRequest(BaseModel):
|
||||
@@ -346,9 +346,9 @@ class HotOpeningGenerateVideoPromptRequest(BaseModel):
|
||||
)
|
||||
|
||||
engine_id: str | None = Field(None, description="视频引擎ID。用于读取该引擎支持的视频时长、比例、分辨率配置;为空使用最高优先级启用引擎")
|
||||
duration: int | None = Field(None, ge=1, description="希望用于视频提词规划的视频时长,单位秒。为空时优先使用 HOT_OPENING_DEFAULT_VIDEO_DURATION")
|
||||
aspect_ratio: str | None = Field(None, description="希望用于视频提词规划的视频比例。为空时优先使用 HOT_OPENING_DEFAULT_VIDEO_RATIO")
|
||||
resolution: str | None = Field(None, description="希望用于视频提词规划的视频分辨率。为空时优先使用 HOT_OPENING_DEFAULT_VIDEO_RESOLUTION")
|
||||
duration: int | None = Field(None, ge=1, description="希望用于视频提词规划的视频时长,单位秒。具体可选值来自视频引擎 supported_durations;为空时优先使用 HOT_OPENING_DEFAULT_VIDEO_DURATION")
|
||||
aspect_ratio: str | None = Field(None, description="希望用于视频提词规划的视频比例,例如 9:16、16:9、1:1。具体可选值来自视频引擎 supported_ratios;为空时优先使用 HOT_OPENING_DEFAULT_VIDEO_RATIO")
|
||||
resolution: str | None = Field(None, description="希望用于视频提词规划的视频分辨率,例如 480p、720p、1080p。具体可选值来自视频引擎 supported_resolutions;为空时优先使用 HOT_OPENING_DEFAULT_VIDEO_RESOLUTION")
|
||||
target_platform: str | None = Field(None, max_length=64, description="目标平台,例如抖音/快手/小红书。为空时使用 HOT_OPENING_DEFAULT_TARGET_PLATFORM")
|
||||
|
||||
|
||||
@@ -370,7 +370,7 @@ class HotOpeningStepOut(BaseModel):
|
||||
module: str = Field(..., description="模块标识,例如 hot_opening_replicate")
|
||||
step_index: int = Field(..., description="步骤序号:1素材输入、2图片提词、3图片生成、4视频提词、5视频生成")
|
||||
step_code: str = Field(..., description="步骤编码:material_input/image_prompt_optimize/image_generate/video_prompt_optimize/video_generate")
|
||||
status: str = Field(..., description="步骤状态:pending/waiting_user/processing/completed/failed/cancelled")
|
||||
status: str = Field(..., description="步骤状态:pending=待处理,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消")
|
||||
version: int = Field(..., description="步骤版本号。重新生成或修改上游步骤后 version+1")
|
||||
is_current: bool = Field(..., description="是否当前有效步骤。旧步骤会软删除且 is_current=false")
|
||||
parent_step_id: str | None = Field(None, description="上一个步骤ID")
|
||||
@@ -427,7 +427,7 @@ class HotOpeningTaskDetailOut(BaseModel):
|
||||
project_id: str = Field(..., description="兼容前端命名,等同于 id")
|
||||
module: str = Field(..., description="模块标识,爆款开头复刻固定为 hot_opening_replicate")
|
||||
title: str | None = Field(None, description="项目标题,默认取生成项目名称")
|
||||
status: str = Field(..., description="总任务状态:pending/waiting_user/processing/completed/failed/cancelled")
|
||||
status: str = Field(..., description="总任务状态:pending=已创建,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消")
|
||||
current_step_code: str | None = Field(None, description="当前所处步骤编码")
|
||||
final_image_url: str | None = Field(None, description="最终新项目图片 URL")
|
||||
final_video_url: str | None = Field(None, description="最终视频 URL")
|
||||
@@ -447,7 +447,7 @@ class HotOpeningTaskListItemOut(BaseModel):
|
||||
project_id: str = Field(..., description="兼容前端命名,等同于 id")
|
||||
module: str = Field(..., description="模块标识")
|
||||
title: str | None = Field(None, description="项目标题")
|
||||
status: str = Field(..., description="总任务状态")
|
||||
status: str = Field(..., description="总任务状态:pending=已创建,waiting_user=等待用户操作,processing=处理中,completed=完成,failed=失败,cancelled=取消")
|
||||
current_step_code: str | None = Field(None, description="当前步骤")
|
||||
target_project_name: str | None = Field(None, description="生成项目名称,来源于第1步素材输入")
|
||||
final_image_url: str | None = Field(None, description="最终图片 URL")
|
||||
|
||||
@@ -208,27 +208,30 @@ class ShotReplicateTaskCreate(BaseModel):
|
||||
|
||||
|
||||
class ShotReplicateMaterialUpdateRequest(BaseModel):
|
||||
"""修改拆镜复刻第1步素材输入请求体。"""
|
||||
"""修改拆镜复刻第1步素材输入请求体。
|
||||
|
||||
拆镜复刻的素材视频来自拆镜片段 segment_video_url,与片段强绑定,不允许修改。
|
||||
本接口只允许修改新产品图片、参考素材项目名、生成项目名和核心内容点。
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"material_video_url": "https://example.com/new-source.mp4",
|
||||
"material_image_url": "https://example.com/new-product.png",
|
||||
"source_project_name": "新的参考素材项目名称",
|
||||
"target_project_name": "新的生成项目名称",
|
||||
"core_content_point": "新的50字以内核心内容点",
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
material_video_url: str | None = Field(None, min_length=1, description="素材视频链接,未传则沿用旧值")
|
||||
material_image_url: str | None = Field(None, min_length=1, description="素材图片链接,未传则沿用旧值")
|
||||
material_image_url: str | None = Field(None, min_length=1, description="素材图片链接,未传则沿用旧值。用于新产品/目标素材图片,来自已有上传接口")
|
||||
source_project_name: str | None = Field(None, min_length=1, max_length=20, description="视频素材内容项目名称,未传则沿用旧值")
|
||||
target_project_name: str | None = Field(None, min_length=1, max_length=20, description="生成项目名称,未传则沿用旧值")
|
||||
core_content_point: str | None = Field(None, min_length=1, max_length=50, description="生成项目核心内容点,最多50字,未传则沿用旧值")
|
||||
|
||||
@field_validator("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point", mode="before")
|
||||
@field_validator("material_image_url", "source_project_name", "target_project_name", "core_content_point", mode="before")
|
||||
@classmethod
|
||||
def _strip_optional(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
@@ -240,7 +243,7 @@ class ShotReplicateMaterialUpdateRequest(BaseModel):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _require_at_least_one(self) -> "ShotReplicateMaterialUpdateRequest":
|
||||
if not any(getattr(self, field) is not None for field in ("material_video_url", "material_image_url", "source_project_name", "target_project_name", "core_content_point")):
|
||||
if not any(getattr(self, field) is not None for field in ("material_image_url", "source_project_name", "target_project_name", "core_content_point")):
|
||||
raise ValueError("至少需要传入一个需要修改的字段")
|
||||
return self
|
||||
|
||||
@@ -248,7 +251,7 @@ class ShotReplicateMaterialUpdateRequest(BaseModel):
|
||||
class ShotReplicateStepUpdate(BaseModel):
|
||||
"""修改拆镜复刻子任务请求体。"""
|
||||
|
||||
material_video_url: str | None = Field(None, description="修改第1步素材视频链接")
|
||||
material_video_url: str | None = Field(None, description="历史兼容字段:拆镜复刻项目素材视频与片段绑定,正式接口不允许修改")
|
||||
material_image_url: str | None = Field(None, description="修改第1步素材图片链接")
|
||||
source_project_name: str | None = Field(None, max_length=20, description="修改第1步视频素材内容项目名称")
|
||||
target_project_name: str | None = Field(None, max_length=20, description="修改第1步生成项目名称")
|
||||
@@ -329,9 +332,9 @@ class ShotReplicateGenerateImageRequest(BaseModel):
|
||||
)
|
||||
|
||||
engine_id: str | None = Field(None, description="图片生成引擎ID。为空则使用当前启用且优先级最高的图片引擎")
|
||||
image_size: str | None = Field(None, description="图片分辨率档位,例如 1K、2K。为空使用引擎默认值")
|
||||
image_proportion: str | None = Field(None, description="图片比例,例如 1:1、16:9、9:16。为空使用默认值")
|
||||
image_px: str | None = Field(None, description="图片像素尺寸,例如 2048x2048。为空时按引擎支持尺寸自动匹配")
|
||||
image_size: str | None = Field(None, description="图片分辨率档位,例如 1K、2K。具体可选值来自图片引擎配置接口;为空使用引擎默认值")
|
||||
image_proportion: str | None = Field(None, description="图片比例,例如 1:1、16:9、9:16。具体可选值来自图片引擎配置接口;为空使用默认值")
|
||||
image_px: str | None = Field(None, description="图片像素尺寸,例如 1024x1024、2048x2048。具体可选值来自图片引擎配置接口;为空时按引擎支持尺寸自动匹配")
|
||||
|
||||
|
||||
class ShotReplicateGenerateVideoPromptRequest(BaseModel):
|
||||
@@ -346,9 +349,9 @@ class ShotReplicateGenerateVideoPromptRequest(BaseModel):
|
||||
)
|
||||
|
||||
engine_id: str | None = Field(None, description="视频引擎ID。用于读取该引擎支持的视频时长、比例、分辨率配置;为空使用最高优先级启用引擎")
|
||||
duration: int | None = Field(None, ge=1, description="希望用于视频提词规划的视频时长,单位秒。为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_DURATION")
|
||||
aspect_ratio: str | None = Field(None, description="希望用于视频提词规划的视频比例。为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_RATIO")
|
||||
resolution: str | None = Field(None, description="希望用于视频提词规划的视频分辨率。为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_RESOLUTION")
|
||||
duration: int | None = Field(None, ge=1, description="希望用于视频提词规划的视频时长,单位秒。具体可选值来自视频引擎 supported_durations;为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_DURATION")
|
||||
aspect_ratio: str | None = Field(None, description="希望用于视频提词规划的视频比例,例如 9:16、16:9、1:1。具体可选值来自视频引擎 supported_ratios;为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_RATIO")
|
||||
resolution: str | None = Field(None, description="希望用于视频提词规划的视频分辨率,例如 480p、720p、1080p。具体可选值来自视频引擎 supported_resolutions;为空时优先使用 SHOT_REPLICATE_DEFAULT_VIDEO_RESOLUTION")
|
||||
target_platform: str | None = Field(None, max_length=64, description="目标平台,例如抖音/快手/小红书。为空时使用 SHOT_REPLICATE_DEFAULT_TARGET_PLATFORM")
|
||||
|
||||
|
||||
@@ -477,13 +480,6 @@ class ShotReplicateDeleteOut(BaseModel):
|
||||
deleted: bool = Field(..., description="是否已软删除")
|
||||
|
||||
|
||||
class ShotReplicateSpecOut(BaseModel):
|
||||
project_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_PROJECT_STATUS_DESCRIPTIONS, description="总任务状态说明")
|
||||
step_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_STATUS_DESCRIPTIONS, description="子任务状态说明")
|
||||
steps: list[dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_DESCRIPTIONS, description="5个固定步骤说明")
|
||||
step_io_schema_version: str = Field(default=SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, description="步骤 input_json/output_json 结构版本")
|
||||
step_io_examples: dict[str, dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_IO_EXAMPLES, description="每个步骤 input_json/output_json 示例")
|
||||
|
||||
|
||||
# ========================
|
||||
# 拆镜总任务集 / 片段 API Schema
|
||||
@@ -579,10 +575,10 @@ class ShotTaskSetCreate(BaseModel):
|
||||
|
||||
|
||||
class ShotTaskSetListQuery(BaseModel):
|
||||
status: str | None = Field(None, description="总任务状态筛选,见 ShotTaskSetStatusEnum")
|
||||
analysis_status: str | None = Field(None, description="分析状态筛选,见 ShotAnalysisStatusEnum")
|
||||
split_status: str | None = Field(None, description="拆镜状态筛选,见 ShotSplitStatusEnum")
|
||||
keyword: str | None = Field(None, description="标题/内容关键词")
|
||||
status: str | None = Field(None, description="总任务状态筛选:pending_analysis/analyzing/analysis_completed/analysis_failed/splitting/split_completed/partial_failed/failed/deleted")
|
||||
analysis_status: str | None = Field(None, description="分析状态筛选:pending/processing/completed/failed")
|
||||
split_status: str | None = Field(None, description="拆镜状态筛选:none/pending/processing/completed/failed/retry_waiting")
|
||||
keyword: str | None = Field(None, description="标题/内容关键词,模糊搜索")
|
||||
page: int = Field(1, ge=1, description="页码")
|
||||
page_size: int = Field(20, ge=1, le=100, description="每页数量")
|
||||
|
||||
@@ -590,44 +586,44 @@ class ShotTaskSetListQuery(BaseModel):
|
||||
class ShotTaskSetOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
title: str | None = None
|
||||
video_url: str
|
||||
video_duration_seconds: float
|
||||
status: str
|
||||
analysis_status: str
|
||||
split_status: str
|
||||
original_video_content: str | None = None
|
||||
original_video_category: str | None = None
|
||||
original_video_audience: str | None = None
|
||||
segment_count: int = 0
|
||||
completed_segment_count: int = 0
|
||||
failed_segment_count: int = 0
|
||||
analysis_error_message: str | None = None
|
||||
split_error_message: str | None = None
|
||||
created_at: NaiveDatetimeOptional = None
|
||||
updated_at: NaiveDatetimeOptional = None
|
||||
id: str = Field(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id")
|
||||
title: str | None = Field(None, description="拆镜总任务标题,可为空")
|
||||
video_url: str = Field(..., description="原视频 URL,来自已有上传接口")
|
||||
video_duration_seconds: float = Field(..., description="原视频时长,单位秒,允许浮点")
|
||||
status: str = Field(..., description="总任务状态:pending_analysis=等待分析,analyzing=分析中,analysis_completed=分析完成,analysis_failed=分析失败,splitting=拆镜中,split_completed=拆镜完成,partial_failed=部分失败,failed=失败,deleted=已软删")
|
||||
analysis_status: str = Field(..., description="原视频分析状态:pending=待分析,processing=分析中,completed=分析完成,failed=分析失败")
|
||||
split_status: str = Field(..., description="拆镜状态:none=尚未拆镜,pending=待拆镜,processing=拆镜中,completed=拆镜完成,failed=拆镜失败,retry_waiting=等待恢复重试")
|
||||
original_video_content: str | None = Field(None, description="AI 分析出的原视频整体内容描述")
|
||||
original_video_category: str | None = Field(None, description="AI 分析出的原视频分类,例如游戏视频、产品广告、教程等")
|
||||
original_video_audience: str | None = Field(None, description="AI 分析出的原视频受众人群")
|
||||
segment_count: int = Field(0, description="当前有效拆镜片段总数")
|
||||
completed_segment_count: int = Field(0, description="切割完成的片段数量")
|
||||
failed_segment_count: int = Field(0, description="切割失败的片段数量")
|
||||
analysis_error_message: str | None = Field(None, description="原视频 AI 分析失败原因")
|
||||
split_error_message: str | None = Field(None, description="总任务级拆镜失败原因")
|
||||
created_at: NaiveDatetimeOptional = Field(None, description="创建时间")
|
||||
updated_at: NaiveDatetimeOptional = Field(None, description="更新时间")
|
||||
|
||||
|
||||
class ShotTaskSetListOut(BaseModel):
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
items: list[ShotTaskSetOut]
|
||||
total: int = Field(..., description="符合筛选条件的总任务集总数")
|
||||
page: int = Field(..., description="当前页码")
|
||||
page_size: int = Field(..., description="每页数量")
|
||||
items: list[ShotTaskSetOut] = Field(default_factory=list, description="拆镜总任务集列表")
|
||||
|
||||
|
||||
class ShotTaskSetDetailOut(ShotTaskSetOut):
|
||||
ai_suggestions: list[ShotAISuggestionOut] = Field(default_factory=list)
|
||||
analysis_result_json: dict[str, Any] | list[Any] | None = None
|
||||
ai_suggestions: list[ShotAISuggestionOut] = Field(default_factory=list, description="AI 建议拆镜时间段列表,split-by-ai 接口可按 index 选择")
|
||||
analysis_result_json: dict[str, Any] | list[Any] | None = Field(None, description="原视频 AI 分析完整 JSON 结果,结构由模型响应决定")
|
||||
|
||||
|
||||
class ShotSplitByAIRequest(BaseModel):
|
||||
selected_indices: list[int] | None = Field(None, description="指定 AI 建议序号。不传则全部拆")
|
||||
replace_existing: bool = Field(False, description="是否软删旧 AI 建议片段后重新拆")
|
||||
selected_indices: list[int] | None = Field(None, description="指定 AI 建议序号列表,序号来自 ai_suggestions[].index;不传则按全部 AI 建议拆镜")
|
||||
replace_existing: bool = Field(False, description="是否软删旧 AI 建议片段后重新拆;false 时保留旧片段并新增未存在片段")
|
||||
|
||||
|
||||
class ShotSplitCustomRequest(BaseModel):
|
||||
start_second: float = Field(..., ge=0, description="自定义拆镜开始秒,允许浮点")
|
||||
start_second: float = Field(..., ge=0, description="自定义拆镜开始秒,允许浮点,必须大于等于0")
|
||||
end_second: float = Field(..., gt=0, description="自定义拆镜结束秒,允许浮点,必须大于 start_second")
|
||||
|
||||
@model_validator(mode="after")
|
||||
@@ -640,56 +636,56 @@ class ShotSplitCustomRequest(BaseModel):
|
||||
class ShotSegmentOut(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: str
|
||||
task_set_id: str
|
||||
segment_index: int
|
||||
segment_name: str | None = None
|
||||
source_mode: str
|
||||
start_second: float
|
||||
end_second: float
|
||||
duration_seconds: float
|
||||
time_node: str
|
||||
split_status: str
|
||||
analysis_status: str
|
||||
replicate_status: str
|
||||
segment_video_url: str | None = None
|
||||
original_video_content: str | None = None
|
||||
original_video_category: str | None = None
|
||||
original_video_audience: str | None = None
|
||||
segment_content: str | None = None
|
||||
segment_category: str | None = None
|
||||
segment_audience: str | None = None
|
||||
split_retry_count: int = 0
|
||||
split_last_error: str | None = None
|
||||
analysis_error_message: str | None = None
|
||||
module_project_id: str | None = None
|
||||
created_at: NaiveDatetimeOptional = None
|
||||
updated_at: NaiveDatetimeOptional = None
|
||||
id: str = Field(..., description="拆镜片段ID,即 shot_replicate_segments.id")
|
||||
task_set_id: str = Field(..., description="所属拆镜总任务集ID,即 shot_replicate_task_sets.id")
|
||||
segment_index: int = Field(..., description="片段序号,从1开始")
|
||||
segment_name: str | None = Field(None, description="片段名称,可为空")
|
||||
source_mode: str = Field(..., description="片段来源:ai_suggestion=AI 建议拆镜,custom=用户自定义拆镜")
|
||||
start_second: float = Field(..., description="片段开始秒")
|
||||
end_second: float = Field(..., description="片段结束秒")
|
||||
duration_seconds: float = Field(..., description="片段时长,单位秒")
|
||||
time_node: str = Field(..., description="片段时间节点展示文案,例如 0-5秒")
|
||||
split_status: str = Field(..., description="切割状态:none=尚未拆镜,pending=待拆镜,processing=拆镜中,completed=拆镜完成,failed=拆镜失败,retry_waiting=等待恢复重试")
|
||||
analysis_status: str = Field(..., description="片段分析状态:not_required=无需单独分析,pending=等待分析,processing=分析中,completed=分析完成,failed=分析失败")
|
||||
replicate_status: str = Field(..., description="片段复刻状态:not_started=未复刻,project_created=已创建复刻项目,processing=复刻处理中,completed=复刻完成,failed=复刻失败")
|
||||
segment_video_url: str | None = Field(None, description="切割后的片段视频 URL。切割完成后有值,用作复刻项目锁定素材视频")
|
||||
original_video_content: str | None = Field(None, description="原视频整体内容描述,来自总任务 AI 分析")
|
||||
original_video_category: str | None = Field(None, description="原视频分类,来自总任务 AI 分析")
|
||||
original_video_audience: str | None = Field(None, description="原视频受众,来自总任务 AI 分析")
|
||||
segment_content: str | None = Field(None, description="当前片段内容描述")
|
||||
segment_category: str | None = Field(None, description="当前片段分类")
|
||||
segment_audience: str | None = Field(None, description="当前片段受众人群")
|
||||
split_retry_count: int = Field(0, description="切割失败后的恢复重试次数")
|
||||
split_last_error: str | None = Field(None, description="最近一次切割失败原因")
|
||||
analysis_error_message: str | None = Field(None, description="片段分析失败原因")
|
||||
module_project_id: str | None = Field(None, description="由该片段创建的拆镜复刻项目ID,即 module_generation_projects.id")
|
||||
created_at: NaiveDatetimeOptional = Field(None, description="创建时间")
|
||||
updated_at: NaiveDatetimeOptional = Field(None, description="更新时间")
|
||||
|
||||
|
||||
class ShotSegmentDetailOut(ShotSegmentOut):
|
||||
analysis_json: dict[str, Any] | list[Any] | None = None
|
||||
ai_suggestion_json: dict[str, Any] | list[Any] | None = None
|
||||
analysis_json: dict[str, Any] | list[Any] | None = Field(None, description="片段 AI 分析完整 JSON,custom 片段可能有值;AI 建议片段通常复用 ai_suggestion_json")
|
||||
ai_suggestion_json: dict[str, Any] | list[Any] | None = Field(None, description="AI 建议拆镜原始 JSON,source_mode=ai_suggestion 时通常有值")
|
||||
|
||||
|
||||
class ShotSegmentListOut(BaseModel):
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
items: list[ShotSegmentOut]
|
||||
total: int = Field(..., description="符合筛选条件的片段总数")
|
||||
page: int = Field(..., description="当前页码")
|
||||
page_size: int = Field(..., description="每页数量")
|
||||
items: list[ShotSegmentOut] = Field(default_factory=list, description="拆镜片段列表")
|
||||
|
||||
|
||||
class ShotSplitByAIOut(BaseModel):
|
||||
task_set_id: str
|
||||
status: str
|
||||
split_status: str
|
||||
created_segment_count: int
|
||||
segments: list[ShotSegmentOut]
|
||||
task_set_id: str = Field(..., description="拆镜总任务集ID")
|
||||
status: str = Field(..., description="总任务状态:pending_analysis/analyzing/analysis_completed/analysis_failed/splitting/split_completed/partial_failed/failed/deleted")
|
||||
split_status: str = Field(..., description="拆镜状态:none/pending/processing/completed/failed/retry_waiting")
|
||||
created_segment_count: int = Field(..., description="本次创建的拆镜片段数量")
|
||||
segments: list[ShotSegmentOut] = Field(default_factory=list, description="本次创建或返回的拆镜片段列表")
|
||||
|
||||
|
||||
class ShotSplitCustomOut(BaseModel):
|
||||
task_set_id: str
|
||||
segment: ShotSegmentOut
|
||||
task_set_id: str = Field(..., description="拆镜总任务集ID")
|
||||
segment: ShotSegmentOut = Field(..., description="本次创建的自定义拆镜片段")
|
||||
|
||||
|
||||
class ShotSegmentReplicationCreateRequest(BaseModel):
|
||||
@@ -704,10 +700,10 @@ class ShotSegmentReplicationCreateRequest(BaseModel):
|
||||
}
|
||||
)
|
||||
|
||||
target_project_name: str = Field(..., min_length=1, max_length=20, description="生成项目名称")
|
||||
core_content_point: str = Field(..., min_length=1, max_length=50, description="生成项目核心内容点,最多50字")
|
||||
material_image_url: str = Field(..., min_length=1, description="新产品/目标素材图片链接,来自已有上传接口")
|
||||
idempotency_key: str | None = Field(None, max_length=64, description="创建 ModuleGenerationProject 幂等键")
|
||||
target_project_name: str = Field(..., min_length=1, max_length=20, description="生成项目名称,最多20字;会写入 module_generation_projects.title 和第1步 material_input")
|
||||
core_content_point: str = Field(..., min_length=1, max_length=50, description="生成项目核心内容点,最多50字;用于后续图片 AI 提词和视频 AI 提词")
|
||||
material_image_url: str = Field(..., min_length=1, description="新产品/目标素材图片链接,来自已有上传接口;作为图片生成参考素材")
|
||||
idempotency_key: str | None = Field(None, max_length=64, description="创建 ModuleGenerationProject 幂等键;为空时后端可按业务生成或不使用")
|
||||
|
||||
@field_validator("target_project_name", "core_content_point", "material_image_url", "idempotency_key", mode="before")
|
||||
@classmethod
|
||||
@@ -721,14 +717,14 @@ class ShotSegmentReplicationCreateRequest(BaseModel):
|
||||
|
||||
|
||||
class ShotReplicateSpecOut(BaseModel):
|
||||
project_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_PROJECT_STATUS_DESCRIPTIONS)
|
||||
step_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_STATUS_DESCRIPTIONS)
|
||||
steps: list[dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_DESCRIPTIONS)
|
||||
step_io_schema_version: str = SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION
|
||||
step_io_examples: dict[str, dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_IO_EXAMPLES)
|
||||
task_set_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_TASK_SET_STATUS_DESCRIPTIONS)
|
||||
analysis_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_ANALYSIS_STATUS_DESCRIPTIONS)
|
||||
split_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SPLIT_STATUS_DESCRIPTIONS)
|
||||
segment_source_modes: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_SOURCE_MODE_DESCRIPTIONS)
|
||||
segment_analysis_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_ANALYSIS_STATUS_DESCRIPTIONS)
|
||||
segment_replicate_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_REPLICATE_STATUS_DESCRIPTIONS)
|
||||
project_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_PROJECT_STATUS_DESCRIPTIONS, description="ModuleGenerationProject 总任务状态说明:pending/waiting_user/processing/completed/failed/cancelled")
|
||||
step_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_STATUS_DESCRIPTIONS, description="ModuleGenerationStep 子任务状态说明:pending/waiting_user/processing/completed/failed/cancelled")
|
||||
steps: list[dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_DESCRIPTIONS, description="5个固定步骤说明:material_input/image_prompt_optimize/image_generate/video_prompt_optimize/video_generate")
|
||||
step_io_schema_version: str = Field(default=SHOT_REPLICATE_STEP_IO_SCHEMA_VERSION, description="步骤 input_json/output_json 结构版本")
|
||||
step_io_examples: dict[str, dict[str, Any]] = Field(default_factory=lambda: SHOT_REPLICATE_STEP_IO_EXAMPLES, description="每个步骤 input_json/output_json 示例")
|
||||
task_set_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_TASK_SET_STATUS_DESCRIPTIONS, description="拆镜总任务集状态说明")
|
||||
analysis_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_ANALYSIS_STATUS_DESCRIPTIONS, description="原视频/片段视频分析状态说明")
|
||||
split_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SPLIT_STATUS_DESCRIPTIONS, description="ffmpeg 拆镜状态说明")
|
||||
segment_source_modes: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_SOURCE_MODE_DESCRIPTIONS, description="拆镜片段来源说明")
|
||||
segment_analysis_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_ANALYSIS_STATUS_DESCRIPTIONS, description="拆镜片段分析状态说明")
|
||||
segment_replicate_statuses: dict[str, str] = Field(default_factory=lambda: SHOT_SEGMENT_REPLICATE_STATUS_DESCRIPTIONS, description="拆镜片段进入复刻流程后的状态说明")
|
||||
|
||||
@@ -717,8 +717,10 @@ async def update_shot_replicate_material_input(
|
||||
old_material_step = await _get_current_step_by_code(db, project.id, ShotReplicateStepCodeEnum.MATERIAL_INPUT.value)
|
||||
old_material = _step_payload(old_material_step.input_json if old_material_step else None)
|
||||
|
||||
# 拆镜复刻的素材视频来自 shot_replicate_segments.segment_video_url,
|
||||
# 与片段强绑定,不允许前端在项目素材修改接口中覆盖。
|
||||
material = {
|
||||
"material_video_url": req.material_video_url if req.material_video_url is not None else old_material.get("material_video_url"),
|
||||
"material_video_url": old_material.get("material_video_url"),
|
||||
"material_image_url": req.material_image_url if req.material_image_url is not None else old_material.get("material_image_url"),
|
||||
"source_project_name": req.source_project_name if req.source_project_name is not None else old_material.get("source_project_name"),
|
||||
"target_project_name": req.target_project_name if req.target_project_name is not None else old_material.get("target_project_name"),
|
||||
|
||||
Reference in New Issue
Block a user