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