Files
video-gen/video-gen-api/app/services/module_generation_step_update_service.py

370 lines
15 KiB
Python

from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import Any
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.common import ModuleEventTypeEnum, ModuleProjectStatusEnum, ModuleStepStatusEnum
from app.models.module_generation_project import ModuleGenerationProject
from app.models.module_generation_step import ModuleGenerationStep
from app.models.user import User
from app.services.module_generation_flow_base_service import (
create_module_step,
get_current_step_by_code,
get_project_for_user,
get_step_for_user,
soft_delete_steps_from_index,
)
from app.enums.module_generation_flow import ModuleGenerationFlowConfig
from app.services.video_prompt_schema_config_service import fallback_runtime_schema_snapshot
from app.services.module_generation_step_common_service import (
build_step_input,
build_step_output,
force_set_json,
merge_dict,
step_payload,
step_usage,
unwrap_step_output,
utc_now,
)
LogModuleEventCallable = Callable[..., Awaitable[None]]
PatchVideoPromptSchemaCallable = Callable[..., dict[str, Any]]
BuildFinalVideoPromptCallable = Callable[[dict[str, Any]], str]
async def update_module_step(
db: AsyncSession,
*,
current_user: User,
project_id: str,
step_id: str,
req: Any,
config: ModuleGenerationFlowConfig,
log_module_event: LogModuleEventCallable,
) -> tuple[ModuleGenerationProject, ModuleGenerationStep]:
project = await get_project_for_user(db, project_id=project_id, user=current_user, config=config, for_update=True)
step = await get_step_for_user(db, project_id=project_id, step_id=step_id, user=current_user, config=config, for_update=True)
if step.status == ModuleStepStatusEnum.PROCESSING.value:
raise HTTPException(status_code=400, detail="当前子任务正在处理中,暂不能修改")
input_data = step_payload(step.input_json, schema_version=config.step_io_schema_version)
output_data = unwrap_step_output(step.output_json, schema_version=config.step_io_schema_version)
if step.step_code == config.material_step_code:
input_data = merge_dict(
input_data,
{
"material_video_url": getattr(req, "material_video_url", None),
"material_image_url": getattr(req, "material_image_url", None),
"source_project_name": getattr(req, "source_project_name", None),
"target_project_name": getattr(req, "target_project_name", None),
"core_content_point": getattr(req, "core_content_point", None),
},
)
target_project_name = getattr(req, "target_project_name", None)
if target_project_name:
project.title = target_project_name
elif step.step_code == config.image_prompt_step_code:
prompt = getattr(req, "prompt", None)
if prompt is not None:
output_data["optimized_prompt"] = prompt
output_data["prompt"] = prompt
elif step.step_code == config.video_prompt_step_code:
prompt_schema = getattr(req, "prompt_schema", None)
prompt = getattr(req, "prompt", None)
if prompt_schema is not None:
output_data["prompt_schema"] = prompt_schema
if prompt is not None:
output_data["final_prompt"] = prompt
else:
if getattr(req, "input_json", None):
input_data = merge_dict(input_data, req.input_json)
if getattr(req, "output_json", None):
output_data = merge_dict(output_data, req.output_json)
if getattr(req, "input_json", None):
input_data = merge_dict(input_data, req.input_json)
if getattr(req, "output_json", None):
output_data = merge_dict(output_data, req.output_json)
force_set_json(
step,
"input_json",
build_step_input(
step_code=step.step_code,
payload=input_data,
source_step_id=step.source_step_id,
parent_step_id=step.parent_step_id,
schema_version=config.step_io_schema_version,
),
)
force_set_json(
step,
"output_json",
build_step_output(step_code=step.step_code, status=ModuleStepStatusEnum.COMPLETED.value, payload=output_data, schema_version=config.step_io_schema_version),
)
step.status = ModuleStepStatusEnum.COMPLETED.value
step.error_message = None
step.completed_at = utc_now()
project.status = ModuleProjectStatusEnum.WAITING_USER.value
project.current_step_code = step.step_code
project.error_message = None
await soft_delete_steps_from_index(
db,
project=project,
start_index=step.step_index + 1,
config=config,
log_module_event=log_module_event,
)
await log_module_event(db, project=project, step=step, event_type=ModuleEventTypeEnum.STEP_UPDATED.value, message="用户修改子任务内容")
await db.flush()
return project, step
async def update_module_material_input(
db: AsyncSession,
*,
current_user: User,
project_id: str,
req: Any,
config: ModuleGenerationFlowConfig,
log_module_event: LogModuleEventCallable,
) -> tuple[str, str]:
project = await get_project_for_user(db, project_id=project_id, user=current_user, config=config, for_update=True)
old_material_step = await get_current_step_by_code(db, project_id=project.id, step_code=config.material_step_code, config=config)
old_material = step_payload(old_material_step.input_json if old_material_step else None, schema_version=config.step_io_schema_version)
material_video_url = (
getattr(req, "material_video_url", None)
if config.material_video_url_editable and getattr(req, "material_video_url", None) is not None
else old_material.get("material_video_url")
)
material = {
"material_video_url": material_video_url,
"material_image_url": getattr(req, "material_image_url", None) if getattr(req, "material_image_url", None) is not None else old_material.get("material_image_url"),
"source_project_name": getattr(req, "source_project_name", None) if getattr(req, "source_project_name", None) is not None else old_material.get("source_project_name"),
"target_project_name": getattr(req, "target_project_name", None) if getattr(req, "target_project_name", None) is not None else old_material.get("target_project_name"),
"core_content_point": getattr(req, "core_content_point", None) if getattr(req, "core_content_point", None) is not None else old_material.get("core_content_point"),
}
missing_fields = [key for key, value in material.items() if value is None or str(value).strip() == ""]
if missing_fields:
raise HTTPException(status_code=400, detail=f"素材输入缺少必要字段: {', '.join(missing_fields)}")
await soft_delete_steps_from_index(
db,
project=project,
start_index=config.step_index_map[config.material_step_code],
config=config,
log_module_event=log_module_event,
)
project.title = str(material["target_project_name"])
project.status = ModuleProjectStatusEnum.WAITING_USER.value
project.current_step_code = config.material_step_code
project.final_image_url = None
project.final_video_url = None
project.final_video_cover_url = None
project.error_message = None
project.completed_at = None
new_step = await create_module_step(
db,
project=project,
step_code=config.material_step_code,
config=config,
log_module_event=log_module_event,
status=ModuleStepStatusEnum.COMPLETED.value,
input_data=material,
output_data={"message": "素材输入已修改,旧步骤已软删除。下一步请重新生成图片AI提词。"},
)
await log_module_event(
db,
project=project,
step=new_step,
event_type=ModuleEventTypeEnum.STEP_UPDATED.value,
message="用户修改素材输入并重建第1步新版本",
detail={
"old_material_step_id": old_material_step.id if old_material_step else None,
"new_material_step_id": new_step.id,
"version": new_step.version,
},
)
return project.id, new_step.id
async def update_module_image_prompt(
db: AsyncSession,
*,
current_user: User,
project_id: str,
step_id: str,
req: Any,
config: ModuleGenerationFlowConfig,
log_module_event: LogModuleEventCallable,
) -> tuple[ModuleGenerationProject, ModuleGenerationStep]:
project = await get_project_for_user(db, project_id=project_id, user=current_user, config=config, for_update=True)
step = await get_step_for_user(db, project_id=project_id, step_id=step_id, user=current_user, config=config, for_update=True)
if step.step_code != config.image_prompt_step_code:
raise HTTPException(status_code=400, detail="只能修改第2步图片 AI 提词子任务")
if step.status != ModuleStepStatusEnum.COMPLETED.value:
raise HTTPException(status_code=400, detail="图片 AI 提词未完成,不能直接修改")
output_data = step_payload(step.output_json, schema_version=config.step_io_schema_version)
usage = step_usage(step.output_json, schema_version=config.step_io_schema_version)
new_prompt = req.prompt.strip()
output_data["optimized_prompt"] = new_prompt
output_data["prompt"] = new_prompt
output_data["manual_edited"] = True
output_data["manual_edited_at"] = utc_now().isoformat()
force_set_json(
step,
"output_json",
build_step_output(
step_code=config.image_prompt_step_code,
status=ModuleStepStatusEnum.COMPLETED.value,
payload=output_data,
usage=usage,
schema_version=config.step_io_schema_version,
),
)
step.status = ModuleStepStatusEnum.COMPLETED.value
step.error_message = None
step.completed_at = utc_now()
await soft_delete_steps_from_index(
db,
project=project,
start_index=config.step_index_map[config.image_generate_step_code],
config=config,
log_module_event=log_module_event,
)
project.status = ModuleProjectStatusEnum.WAITING_USER.value
project.current_step_code = config.image_prompt_step_code
project.final_image_url = None
project.final_video_url = None
project.final_video_cover_url = None
project.completed_at = None
project.error_message = None
await log_module_event(
db,
project=project,
step=step,
event_type=ModuleEventTypeEnum.STEP_UPDATED.value,
message="用户直接修改图片 AI 优化提词,已软删除后续步骤",
detail={"start_deleted_step_index": config.step_index_map[config.image_generate_step_code]},
)
await db.flush()
return project, step
async def update_module_video_prompt_schema(
db: AsyncSession,
*,
current_user: User,
project_id: str,
step_id: str,
req: Any,
config: ModuleGenerationFlowConfig,
log_module_event: LogModuleEventCallable,
patch_video_prompt_schema_from_client: PatchVideoPromptSchemaCallable,
build_final_video_prompt: BuildFinalVideoPromptCallable,
) -> tuple[ModuleGenerationProject, ModuleGenerationStep]:
project = await get_project_for_user(db, project_id=project_id, user=current_user, config=config, for_update=True)
step = await get_step_for_user(db, project_id=project_id, step_id=step_id, user=current_user, config=config, for_update=True)
if step.step_code != config.video_prompt_step_code:
raise HTTPException(status_code=400, detail="只能修改第4步视频 AI 提词 JSON schema 子任务")
if step.status != ModuleStepStatusEnum.COMPLETED.value:
raise HTTPException(status_code=400, detail="视频 AI 提词未完成,不能直接修改")
output_data = step_payload(step.output_json, schema_version=config.step_io_schema_version)
usage = step_usage(step.output_json, schema_version=config.step_io_schema_version)
input_data = step_payload(step.input_json, schema_version=config.step_io_schema_version)
server_schema = output_data.get("prompt_schema") if isinstance(output_data.get("prompt_schema"), dict) else {}
video_config = output_data.get("params_used_for_prompt") or input_data.get("video_config") or {}
if not isinstance(video_config, dict) or not video_config.get("duration") or not video_config.get("aspect_ratio") or not video_config.get("resolution"):
raise HTTPException(status_code=400, detail="缺少第4步视频参数快照,不能安全修改视频 schema")
schema_config_snapshot = fallback_runtime_schema_snapshot(output_data.get("schema_config_snapshot"))
try:
patched_schema = patch_video_prompt_schema_from_client(
server_schema=server_schema,
client_schema=req.prompt_schema,
video_config=video_config,
schema_config_snapshot=schema_config_snapshot,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
final_prompt = build_final_video_prompt(patched_schema)
output_data["prompt_schema"] = patched_schema
output_data["final_prompt"] = final_prompt
output_data["params_used_for_prompt"] = video_config
output_data["manual_edited"] = True
output_data["manual_edited_at"] = utc_now().isoformat()
force_set_json(
step,
"output_json",
build_step_output(
step_code=config.video_prompt_step_code,
status=ModuleStepStatusEnum.COMPLETED.value,
payload=output_data,
usage=usage,
schema_version=config.step_io_schema_version,
),
)
step.status = ModuleStepStatusEnum.COMPLETED.value
step.error_message = None
step.completed_at = utc_now()
await soft_delete_steps_from_index(
db,
project=project,
start_index=config.step_index_map[config.video_generate_step_code],
config=config,
log_module_event=log_module_event,
)
project.status = ModuleProjectStatusEnum.WAITING_USER.value
project.current_step_code = config.video_prompt_step_code
project.final_video_url = None
project.final_video_cover_url = None
project.completed_at = None
project.error_message = None
await log_module_event(
db,
project=project,
step=step,
event_type=ModuleEventTypeEnum.STEP_UPDATED.value,
message="用户修改视频 AI 提词 schema,已软删除视频生成步骤",
detail={
"start_deleted_step_index": config.step_index_map[config.video_generate_step_code],
"locked_fields": [
"schema_version",
"schema_usage",
"画面属性.视频时长",
"画面属性.视频比例",
"画面属性.清晰度",
"画面属性.帧率",
"画面属性.推荐分辨率",
"动作流程[*].时间段",
"镜头流程[*].时间段",
"动态时间规划",
"输出规格限制",
"质量控制",
"合规控制",
],
},
)
await db.flush()
return project, step