370 lines
15 KiB
Python
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
|