From d9c81a3ac01f2b0d7d789b1581371e584b7e84bf Mon Sep 17 00:00:00 2001 From: www Date: Fri, 18 Sep 2026 08:14:05 +0200 Subject: [PATCH] feat: expand novel IP and production workflows --- .gitignore | 6 + CURRENT_ARCHITECTURE.md | 1808 ++- NOVEL_IP_SYSTEM_DESIGN.md | 1288 ++ PROJECT_STATUS_V1.md | 879 ++ admin/src/App.vue | 4211 +++++- admin/src/api/client.ts | 87 +- admin/src/styles.css | 536 + .../migration.sql | 41 + .../migration.sql | 51 + .../migration.sql | 105 + .../migration.sql | 164 + .../migration.sql | 22 + .../migration.sql | 11 + .../migration.sql | 10 + .../migration.sql | 10 + .../migration.sql | 6 + .../migration.sql | 44 + .../migration.sql | 187 + .../migration.sql | 13 + .../migration.sql | 7 + .../migration.sql | 2 + .../migration.sql | 3 + .../migration.sql | 35 + .../migration.sql | 58 + .../migration.sql | 100 + .../migration.sql | 34 + .../migration.sql | 60 + .../migration.sql | 48 + .../migration.sql | 20 + .../migration.sql | 7 + .../migration.sql | 4 + .../migration.sql | 42 + .../migration.sql | 79 + .../migration.sql | 213 + .../migration.sql | 230 + .../migration.sql | 15 + .../migration.sql | 64 + .../migration.sql | 71 + .../migration.sql | 60 + .../migration.sql | 45 + .../migration.sql | 2 + .../migration.sql | 26 + backend/prisma/schema.prisma | 1407 +- backend/prisma/seed.ts | 325 +- backend/src/admin/admin.controller.ts | 95 + backend/src/admin/admin.dto.ts | 70 + backend/src/admin/admin.service.spec.ts | 359 +- backend/src/admin/admin.service.ts | 1886 ++- backend/src/admin/admin.types.ts | 23 + .../src/ai-router/ai-router.service.spec.ts | 85 + backend/src/ai-router/ai-router.service.ts | 61 +- backend/src/ai-router/ai-router.types.ts | 34 +- backend/src/app.module.ts | 6 + backend/src/assets/asset.types.ts | 31 +- backend/src/assets/assets.controller.ts | 44 +- backend/src/assets/assets.service.spec.ts | 113 +- backend/src/assets/assets.service.ts | 309 +- .../assets/public-temp-assets.controller.ts | 17 +- backend/src/assets/storage.service.ts | 122 +- backend/src/assets/upload.dto.ts | 7 + backend/src/characters/character.dto.ts | 287 + backend/src/characters/character.types.ts | 429 +- .../src/characters/characters.controller.ts | 416 +- backend/src/characters/characters.module.ts | 4 +- .../src/characters/characters.service.spec.ts | 54 + backend/src/characters/characters.service.ts | 7292 +++++++++- ...haracter-turnaround-panel-template.spec.ts | 66 + .../character-turnaround-panel-template.ts | 120 + .../character-turnaround-template.spec.ts | 147 + .../common/character-turnaround-template.ts | 185 + backend/src/common/production-format.ts | 10 + backend/src/episodes/episode.dto.ts | 10 + backend/src/episodes/episodes.controller.ts | 9 + backend/src/episodes/episodes.module.ts | 3 +- backend/src/episodes/episodes.service.ts | 2180 ++- .../generation-plan.module.ts | 10 + .../generation-plan.service.spec.ts | 143 + .../generation-plan.service.ts | 352 + .../generation-plans/generation-plan.types.ts | 57 + ...haracter-turnaround-master.service.spec.ts | 230 + .../character-turnaround-master.service.ts | 558 + backend/src/images/image.dto.ts | 39 + backend/src/images/image.types.ts | 28 + backend/src/images/images.controller.ts | 50 +- backend/src/images/images.module.ts | 3 +- backend/src/images/images.service.spec.ts | 619 +- backend/src/images/images.service.ts | 3606 ++++- .../live-action-provider-acceptance.ts | 11 + .../src/live-action/live-action.controller.ts | 80 +- backend/src/live-action/live-action.dto.ts | 36 +- backend/src/live-action/live-action.module.ts | 4 +- .../live-action/live-action.service.spec.ts | 1338 +- .../src/live-action/live-action.service.ts | 11138 ++++++++++++++- backend/src/live-action/live-action.types.ts | 69 +- .../prompt-builder.service.spec.ts | 114 +- .../src/live-action/prompt-builder.service.ts | 935 +- backend/src/media/media.dto.ts | 2 + backend/src/media/media.service.ts | 44 +- backend/src/memories/memories.controller.ts | 10 +- backend/src/memories/memories.module.ts | 3 +- backend/src/memories/memories.service.ts | 952 +- backend/src/memories/memory.dto.ts | 3 + .../model-registry.controller.ts | 24 + .../model-registry/model-registry.module.ts | 13 + .../model-registry.service.spec.ts | 156 + .../model-registry/model-registry.service.ts | 512 + .../model-registry/model-registry.types.ts | 94 + backend/src/novels/novel-agent.service.ts | 616 + backend/src/novels/novel-agent.types.ts | 122 + .../novels/novel-context-builder.service.ts | 615 + .../novel-creation-wizard.controller.ts | 58 + .../src/novels/novel-creation-wizard.dto.ts | 22 + .../novels/novel-creation-wizard.service.ts | 889 ++ .../novel-generation-workflow.service.ts | 2814 ++++ .../src/novels/novel-generation.controller.ts | 188 + backend/src/novels/novel-generation.dto.ts | 51 + backend/src/novels/novel-generation.types.ts | 260 + .../src/novels/novel-parser.service.spec.ts | 43 + backend/src/novels/novel-parser.service.ts | 118 +- .../novels/novel-prompt-builder.service.ts | 179 + backend/src/novels/novel.dto.ts | 2 + backend/src/novels/novel.types.ts | 39 + backend/src/novels/novels.module.ts | 39 +- backend/src/novels/novels.service.spec.ts | 4 + backend/src/novels/novels.service.ts | 26 +- .../src/novels/original-novel-mock.service.ts | 254 +- backend/src/novels/original-novel.dto.ts | 3 + backend/src/novels/original-novel.types.ts | 2 +- .../contracts/production-contracts.ts | 654 + .../fixtures/neutral-production-fixtures.ts | 512 + backend/src/production-kernel/index.ts | 4 + .../legacy-behavior-guard.spec.ts | 36 + .../production-kernel.controller.ts | 149 + .../production-kernel.dto.ts | 61 + .../production-kernel.module.ts | 16 + .../production-kernel.service.spec.ts | 642 + .../production-kernel.service.ts | 2868 ++++ .../production-kernel.spec.ts | 312 + ...production-stage-prompt-builder.service.ts | 944 ++ .../prompts/prompt-contamination.ts | 57 + .../prompts/prompt-rule-registry.ts | 396 + .../production-contract-validators.ts | 1108 ++ backend/src/projects/project.dto.ts | 64 + backend/src/projects/project.types.ts | 41 +- backend/src/projects/projects.controller.ts | 149 + backend/src/projects/projects.service.spec.ts | 382 + backend/src/projects/projects.service.ts | 2215 ++- .../provider-lab/provider-lab.controller.ts | 45 + backend/src/provider-lab/provider-lab.dto.ts | 14 + .../src/provider-lab/provider-lab.module.ts | 15 + .../src/provider-lab/provider-lab.service.ts | 918 ++ .../providers/openai-image-pricing.spec.ts | 25 + backend/src/providers/openai-image-pricing.ts | 56 + backend/src/providers/provider.dto.ts | 20 + backend/src/providers/provider.types.ts | 1149 +- backend/src/providers/providers.controller.ts | 17 + .../src/providers/providers.service.spec.ts | 1074 +- backend/src/providers/providers.service.ts | 2239 ++- backend/src/queues/queues.service.ts | 19 +- backend/src/queues/task.types.ts | 8 + backend/src/scripts/script.dto.ts | 20 + backend/src/scripts/script.types.ts | 90 + backend/src/scripts/scripts.controller.ts | 36 +- backend/src/scripts/scripts.module.ts | 3 +- backend/src/scripts/scripts.service.spec.ts | 2 +- backend/src/scripts/scripts.service.ts | 3596 ++++- backend/src/story-bibles/story-bible.dto.ts | 4 + backend/src/story-bibles/story-bible.types.ts | 4 + .../story-bibles/story-bibles.controller.ts | 2 +- .../src/story-bibles/story-bibles.module.ts | 3 +- .../src/story-bibles/story-bibles.service.ts | 1295 +- docs/ai成本.md | 135 + docs/chatgpt/AUTO_RULES_AI_SHORT_DRAMA.md | 375 + docs/chatgpt/Actor.md | 556 + docs/chatgpt/CODEX_OPTIMIZATION_NOTES.md | 277 + docs/chatgpt/COURSE_SUMMARY_AI_SHORT_DRAMA.md | 293 + docs/chatgpt/PROMPT_LIBRARY_AI_SHORT_DRAMA.md | 323 + docs/chatgpt/jianyi001md | 548 + docs/chatgpt/novel/1.md | 550 + docs/chatgpt/novel/10.md | 370 + docs/chatgpt/novel/11.md | 661 + docs/chatgpt/novel/12.md | 608 + docs/chatgpt/novel/13.md | 415 + docs/chatgpt/novel/2.md | 1709 +++ docs/chatgpt/novel/3.md | 1183 ++ docs/chatgpt/novel/4.md | 1109 ++ docs/chatgpt/novel/5.md | 250 + docs/chatgpt/novel/6.md | 261 + docs/chatgpt/novel/7.md | 519 + docs/chatgpt/novel/8.md | 511 + docs/chatgpt/novel/9.md | 331 + docs/feishu-tutorial-fetch.md | 952 ++ docs/work-sync/00_文档索引.md | 99 + .../01_项目现状/PROJECT_STATUS_V1.md | 879 ++ .../02_架构设计/SYSTEM_ARCHITECTURE_V1.md | 209 + docs/work-sync/03_小说引擎/NOVEL_ENGINE_V1.md | 147 + .../04_短剧引擎/AI_VIDEO_TEST_LESSONS_V1.md | 323 + docs/work-sync/04_短剧引擎/DRAMA_ENGINE_V1.md | 239 + docs/work-sync/05_AI流水线/AI_PIPELINE_V1.md | 222 + .../06_数据库/DATABASE_CURRENT_V1.md | 260 + docs/work-sync/07_版本记录/CHANGELOG.md | 364 + .../08_维护规范/DOCUMENT_TEMPLATE.md | 83 + .../08_维护规范/WORK_SYNC_PROTOCOL.md | 133 + docs/work-sync/99_历史设计/README.md | 42 + ...UEST_INSPECTOR_IMPLEMENTATION_REPORT_V1.md | 69 + .../KLING_S_PLUS_MODEL_ROUTING_SPEC_V1.0.md | 1237 ++ docs/work-sync/KLING_V3_OMNI_API_UPDATE_V1.md | 59 + ...RACTER_ELEMENT_IMPLEMENTATION_REPORT_V1.md | 148 + docs/work-sync/PROMPT_RULE_AUDIT_V1.md | 116 + docs/work-sync/README.md | 96 + ...HARACTER_TURNAROUND_ITERATION_REPORT_V1.md | 141 + ...US_CHARACTER_TURNAROUND_PROMPT_V4_AUDIT.md | 112 + ...ND_SPLIT_PANEL_IMPLEMENTATION_REPORT_V1.md | 118 + ...UTION_BRIDGE_EXTERNAL_ASSET_WORKFLOW_V1.md | 57 + ...ENERATION_PLAN_IMPLEMENTATION_REPORT_V1.md | 71 + ..._AND_PLAN_DIFF_IMPLEMENTATION_REPORT_V1.md | 95 + .../SPLUS_PHASE_0_1_IMPLEMENTATION_REPORT.md | 141 + .../SPLUS_PHASE_2_IMPLEMENTATION_REPORT.md | 152 + docs/work-sync/SYNC_MANIFEST.md | 78 + ..._BATCH_AUTOMATED_PRODUCTION_PIPELINE_V2.md | 858 ++ ...US_P0_EXECUTION_GATES_IMPLEMENTATION_V1.md | 135 + .../S_PLUS_PRODUCTION_FLOW_AUDIT_V1.md | 626 + ...NE_GEOGRAPHY_BLOCKING_IMPLEMENTATION_V1.md | 160 + ..._PLUS_SHORT_DRAMA_CODEX_OPTIMIZATION_V1.md | 1327 ++ ...LUS_SHORT_DRAMA_CODEX_OPTIMIZATION_V1_1.md | 1152 ++ docs/work-sync/豆包讨论.txt | 2252 +++ .../AiRequestInspector.vue | 304 + .../ai-request-inspector.ts | 298 + user-app/src/api/ai-request-inspector.spec.ts | 96 + user-app/src/api/client.ts | 2605 +++- .../production/SplusProductionPipeline.vue | 1377 ++ .../src/components/tools/CreativeTools.vue | 1395 ++ user-app/src/pages/index/index.vue | 11401 +++++++++++++++- user-app/src/styles.css | 3325 ++++- user-app/src/workflow.ts | 4 + 235 files changed, 117971 insertions(+), 2721 deletions(-) create mode 100644 NOVEL_IP_SYSTEM_DESIGN.md create mode 100644 PROJECT_STATUS_V1.md create mode 100644 backend/prisma/migrations/20260615125000_character_workshop_v1/migration.sql create mode 100644 backend/prisma/migrations/20260615143000_seedance_20_providers/migration.sql create mode 100644 backend/prisma/migrations/20260615152000_volcengine_creation_providers/migration.sql create mode 100644 backend/prisma/migrations/20260615165000_openai_model_pool/migration.sql create mode 100644 backend/prisma/migrations/20260615172000_volcengine_content_generation_video/migration.sql create mode 100644 backend/prisma/migrations/20260616014000_seedance_20_cost_rules/migration.sql create mode 100644 backend/prisma/migrations/20260616121000_seedream_image_size_floor/migration.sql create mode 100644 backend/prisma/migrations/20260616124000_seedream_image_timeout_10m/migration.sql create mode 100644 backend/prisma/migrations/20260617093000_novel_chapter_volumes/migration.sql create mode 100644 backend/prisma/migrations/20260617111500_novel_reader_tools/migration.sql create mode 100644 backend/prisma/migrations/20260617133000_novel_ip_phase1/migration.sql create mode 100644 backend/prisma/migrations/20260617185500_novel_ai_trace/migration.sql create mode 100644 backend/prisma/migrations/20260617193000_novel_design_outline_fields/migration.sql create mode 100644 backend/prisma/migrations/20260617203000_story_bible_tone_text/migration.sql create mode 100644 backend/prisma/migrations/20260617211000_story_bible_production_json/migration.sql create mode 100644 backend/prisma/migrations/20260618062000_character_extraction_versions/migration.sql create mode 100644 backend/prisma/migrations/20260618142000_character_ip_assets/migration.sql create mode 100644 backend/prisma/migrations/20260618165000_ip_video_pipeline_v1/migration.sql create mode 100644 backend/prisma/migrations/20260619093000_character_prompt_versions/migration.sql create mode 100644 backend/prisma/migrations/20260619143000_character_prompt_review_gate/migration.sql create mode 100644 backend/prisma/migrations/20260619162000_model_preferences_project_assets/migration.sql create mode 100644 backend/prisma/migrations/20260619173000_ip_asset_center_v1/migration.sql create mode 100644 backend/prisma/migrations/20260702093000_asset_selection_alias/migration.sql create mode 100644 backend/prisma/migrations/20260702102000_storyboard_transition_to_next/migration.sql create mode 100644 backend/prisma/migrations/20260713133000_character_image_visual_quality_loop/migration.sql create mode 100644 backend/prisma/migrations/20260715143000_splus_production_contracts_v1/migration.sql create mode 100644 backend/prisma/migrations/20260715190000_kling_v3_omni_api_upgrade/migration.sql create mode 100644 backend/prisma/migrations/20260715213000_kling_splus_routing_v1/migration.sql create mode 100644 backend/prisma/migrations/20260715214500_kling_v3_4k_price_correction/migration.sql create mode 100644 backend/prisma/migrations/20260715223000_shot_generation_plan_v1/migration.sql create mode 100644 backend/prisma/migrations/20260715233000_model_registry_versions_v1/migration.sql create mode 100644 backend/prisma/migrations/20260716003000_landscape_character_elements_v1/migration.sql create mode 100644 backend/prisma/migrations/20260716113000_openai_gpt_image2_4k_pricing/migration.sql create mode 100644 backend/prisma/migrations/20260720141500_scene_composer_transition_contract_v2/migration.sql create mode 100644 backend/prisma/migrations/20260720144500_scene_composer_config_v2_backfill/migration.sql create mode 100644 backend/src/common/character-turnaround-panel-template.spec.ts create mode 100644 backend/src/common/character-turnaround-panel-template.ts create mode 100644 backend/src/common/character-turnaround-template.spec.ts create mode 100644 backend/src/common/character-turnaround-template.ts create mode 100644 backend/src/common/production-format.ts create mode 100644 backend/src/generation-plans/generation-plan.module.ts create mode 100644 backend/src/generation-plans/generation-plan.service.spec.ts create mode 100644 backend/src/generation-plans/generation-plan.service.ts create mode 100644 backend/src/generation-plans/generation-plan.types.ts create mode 100644 backend/src/images/character-turnaround-master.service.spec.ts create mode 100644 backend/src/images/character-turnaround-master.service.ts create mode 100644 backend/src/model-registry/model-registry.controller.ts create mode 100644 backend/src/model-registry/model-registry.module.ts create mode 100644 backend/src/model-registry/model-registry.service.spec.ts create mode 100644 backend/src/model-registry/model-registry.service.ts create mode 100644 backend/src/model-registry/model-registry.types.ts create mode 100644 backend/src/novels/novel-agent.service.ts create mode 100644 backend/src/novels/novel-agent.types.ts create mode 100644 backend/src/novels/novel-context-builder.service.ts create mode 100644 backend/src/novels/novel-creation-wizard.controller.ts create mode 100644 backend/src/novels/novel-creation-wizard.dto.ts create mode 100644 backend/src/novels/novel-creation-wizard.service.ts create mode 100644 backend/src/novels/novel-generation-workflow.service.ts create mode 100644 backend/src/novels/novel-generation.controller.ts create mode 100644 backend/src/novels/novel-generation.dto.ts create mode 100644 backend/src/novels/novel-generation.types.ts create mode 100644 backend/src/novels/novel-prompt-builder.service.ts create mode 100644 backend/src/production-kernel/contracts/production-contracts.ts create mode 100644 backend/src/production-kernel/fixtures/neutral-production-fixtures.ts create mode 100644 backend/src/production-kernel/index.ts create mode 100644 backend/src/production-kernel/legacy-behavior-guard.spec.ts create mode 100644 backend/src/production-kernel/production-kernel.controller.ts create mode 100644 backend/src/production-kernel/production-kernel.dto.ts create mode 100644 backend/src/production-kernel/production-kernel.module.ts create mode 100644 backend/src/production-kernel/production-kernel.service.spec.ts create mode 100644 backend/src/production-kernel/production-kernel.service.ts create mode 100644 backend/src/production-kernel/production-kernel.spec.ts create mode 100644 backend/src/production-kernel/production-stage-prompt-builder.service.ts create mode 100644 backend/src/production-kernel/prompts/prompt-contamination.ts create mode 100644 backend/src/production-kernel/prompts/prompt-rule-registry.ts create mode 100644 backend/src/production-kernel/validators/production-contract-validators.ts create mode 100644 backend/src/provider-lab/provider-lab.controller.ts create mode 100644 backend/src/provider-lab/provider-lab.dto.ts create mode 100644 backend/src/provider-lab/provider-lab.module.ts create mode 100644 backend/src/provider-lab/provider-lab.service.ts create mode 100644 backend/src/providers/openai-image-pricing.spec.ts create mode 100644 backend/src/providers/openai-image-pricing.ts create mode 100644 docs/ai成本.md create mode 100644 docs/chatgpt/AUTO_RULES_AI_SHORT_DRAMA.md create mode 100644 docs/chatgpt/Actor.md create mode 100644 docs/chatgpt/CODEX_OPTIMIZATION_NOTES.md create mode 100644 docs/chatgpt/COURSE_SUMMARY_AI_SHORT_DRAMA.md create mode 100644 docs/chatgpt/PROMPT_LIBRARY_AI_SHORT_DRAMA.md create mode 100644 docs/chatgpt/jianyi001md create mode 100644 docs/chatgpt/novel/1.md create mode 100644 docs/chatgpt/novel/10.md create mode 100644 docs/chatgpt/novel/11.md create mode 100644 docs/chatgpt/novel/12.md create mode 100644 docs/chatgpt/novel/13.md create mode 100644 docs/chatgpt/novel/2.md create mode 100644 docs/chatgpt/novel/3.md create mode 100644 docs/chatgpt/novel/4.md create mode 100644 docs/chatgpt/novel/5.md create mode 100644 docs/chatgpt/novel/6.md create mode 100644 docs/chatgpt/novel/7.md create mode 100644 docs/chatgpt/novel/8.md create mode 100644 docs/chatgpt/novel/9.md create mode 100644 docs/feishu-tutorial-fetch.md create mode 100644 docs/work-sync/00_文档索引.md create mode 100644 docs/work-sync/01_项目现状/PROJECT_STATUS_V1.md create mode 100644 docs/work-sync/02_架构设计/SYSTEM_ARCHITECTURE_V1.md create mode 100644 docs/work-sync/03_小说引擎/NOVEL_ENGINE_V1.md create mode 100644 docs/work-sync/04_短剧引擎/AI_VIDEO_TEST_LESSONS_V1.md create mode 100644 docs/work-sync/04_短剧引擎/DRAMA_ENGINE_V1.md create mode 100644 docs/work-sync/05_AI流水线/AI_PIPELINE_V1.md create mode 100644 docs/work-sync/06_数据库/DATABASE_CURRENT_V1.md create mode 100644 docs/work-sync/07_版本记录/CHANGELOG.md create mode 100644 docs/work-sync/08_维护规范/DOCUMENT_TEMPLATE.md create mode 100644 docs/work-sync/08_维护规范/WORK_SYNC_PROTOCOL.md create mode 100644 docs/work-sync/99_历史设计/README.md create mode 100644 docs/work-sync/AI_REQUEST_INSPECTOR_IMPLEMENTATION_REPORT_V1.md create mode 100644 docs/work-sync/KLING_S_PLUS_MODEL_ROUTING_SPEC_V1.0.md create mode 100644 docs/work-sync/KLING_V3_OMNI_API_UPDATE_V1.md create mode 100644 docs/work-sync/LANDSCAPE_CHARACTER_ELEMENT_IMPLEMENTATION_REPORT_V1.md create mode 100644 docs/work-sync/PROMPT_RULE_AUDIT_V1.md create mode 100644 docs/work-sync/README.md create mode 100644 docs/work-sync/SPLUS_CHARACTER_TURNAROUND_ITERATION_REPORT_V1.md create mode 100644 docs/work-sync/SPLUS_CHARACTER_TURNAROUND_PROMPT_V4_AUDIT.md create mode 100644 docs/work-sync/SPLUS_CHARACTER_TURNAROUND_SPLIT_PANEL_IMPLEMENTATION_REPORT_V1.md create mode 100644 docs/work-sync/SPLUS_EXECUTION_BRIDGE_EXTERNAL_ASSET_WORKFLOW_V1.md create mode 100644 docs/work-sync/SPLUS_GENERATION_PLAN_IMPLEMENTATION_REPORT_V1.md create mode 100644 docs/work-sync/SPLUS_MODEL_REGISTRY_AND_PLAN_DIFF_IMPLEMENTATION_REPORT_V1.md create mode 100644 docs/work-sync/SPLUS_PHASE_0_1_IMPLEMENTATION_REPORT.md create mode 100644 docs/work-sync/SPLUS_PHASE_2_IMPLEMENTATION_REPORT.md create mode 100644 docs/work-sync/SYNC_MANIFEST.md create mode 100644 docs/work-sync/S_PLUS_BATCH_AUTOMATED_PRODUCTION_PIPELINE_V2.md create mode 100644 docs/work-sync/S_PLUS_P0_EXECUTION_GATES_IMPLEMENTATION_V1.md create mode 100644 docs/work-sync/S_PLUS_PRODUCTION_FLOW_AUDIT_V1.md create mode 100644 docs/work-sync/S_PLUS_SCENE_GEOGRAPHY_BLOCKING_IMPLEMENTATION_V1.md create mode 100644 docs/work-sync/S_PLUS_SHORT_DRAMA_CODEX_OPTIMIZATION_V1.md create mode 100644 docs/work-sync/S_PLUS_SHORT_DRAMA_CODEX_OPTIMIZATION_V1_1.md create mode 100644 docs/work-sync/豆包讨论.txt create mode 100644 shared/frontend/ai-request-inspector/AiRequestInspector.vue create mode 100644 shared/frontend/ai-request-inspector/ai-request-inspector.ts create mode 100644 user-app/src/api/ai-request-inspector.spec.ts create mode 100644 user-app/src/components/production/SplusProductionPipeline.vue create mode 100644 user-app/src/components/tools/CreativeTools.vue diff --git a/.gitignore b/.gitignore index a633a13..e1abb37 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,12 @@ admin/storage/ user-app/storage/ workers/storage/ +novels/* + +# Local working data and generated quality-control artifacts +data/ +tmp/ + # Generated archives / local exports *.zip *.tar diff --git a/CURRENT_ARCHITECTURE.md b/CURRENT_ARCHITECTURE.md index fbfa807..c9bad14 100644 --- a/CURRENT_ARCHITECTURE.md +++ b/CURRENT_ARCHITECTURE.md @@ -1,933 +1,919 @@ -# Current Architecture +# CURRENT_ARCHITECTURE -生成时间:2026-06-15 -扫描范围:`/www/wwwroot/ai` 源码、Prisma schema、前后台入口、Provider/Router/Live Action/Media 实现。 +扫描时间:2026-06-17 +范围:`backend`、`user-app`、`admin`、`workers`、`backend/prisma`、`deploy`、`tools`。 +说明:本文只记录当前项目结构、实现状态和关键路径,不包含任何 Provider 密钥。 ## 1. 系统整体架构图 -```text -用户端 H5 / uni-app -user-app/src/pages/index/index.vue - | - | REST API + JWT + 可选加密传输 - v -NestJS Backend -backend/src/app.module.ts - | - |---------------- Auth / Users / Billing - |---------------- Projects / Novels / Story Bible - |---------------- Characters / Memories / Episodes - |---------------- Scripts / Images / Media - |---------------- Live Action / AI Router / Providers - |---------------- Reviews / Admin / Queues - | - v -MySQL 8 -Prisma schema: backend/prisma/schema.prisma - | - +--> RenderTask / ProviderLog / OperationLog / Asset - | - v -Storage -local storage: storage/private -MinIO 预留: backend/src/assets/storage.service.ts +```mermaid +flowchart TB + User[用户前台\nllzai.top\nVue 3 + Vite] --> Nginx[Nginx / 宝塔站点] + Admin[管理后台\nai.admin.llzai.top\nVue 3 + Vite] --> Nginx + Nginx --> API[后端 API\nai.api.llzai.top\nNestJS /api] -后台管理端 -admin/src/App.vue - | - | Provider 配置 / Router 审计 / 任务审计 / 用户项目管理 - v -NestJS Admin API + API --> Auth[Auth / User / RBAC] + API --> Project[Projects / Workflow] + API --> Novel[Novel Library / Reader] + API --> Story[StoryBible / Episode / Script / Storyboard] + API --> Character[Character Library / Actor Profile] + API --> Live[Live Action Pipeline] + API --> Provider[AI Provider Layer] + API --> Media[Media / FFmpeg Render] + API --> AdminApi[Admin / Ops / Provider Config] + API --> Billing[Billing / Quota / Provider Cost] -Worker 预留 -workers/src/main.ts - | - | BullMQ 队列 worker 框架 - v -Redis / BullMQ + API --> Prisma[Prisma ORM] + Prisma --> MySQL[(MySQL)] -AI Provider 抽象层 -backend/src/providers/providers.service.ts - | - | mock / OpenAI / DeepSeek / MiniMax / Hailuo / Kling / Runway / Gemini 等 - v -第三方 AI 平台 + API --> Redis[(Redis)] + Redis --> BullMQ[BullMQ Queues] + Worker[workers\nBullMQ Worker] --> BullMQ + Worker --> InternalAPI[/api/internal/worker/tasks/:id/execute] + InternalAPI --> API -FFmpeg - | - | 图片/音频/字幕合成 MP4 - | 真人视频片段拼接、裁切、字幕、BGM、SFX、响度归一 - v -最终视频 Asset + API --> Storage[StorageService\nLocal / MinIO / Public Temp URL] + Storage --> Files[(storage / object files)] + + Provider --> OpenAI[OpenAI] + Provider --> Volc[火山 / 豆包] + Provider --> Kling[可灵 Kling] + Provider --> MiniMax[MiniMax / 海螺] + Provider --> Domestic[DeepSeek / 通义 / Kimi / 智谱等] + Provider --> Mock[Mock Providers] + + Live --> FFmpeg[FFmpeg / FFprobe] + Media --> FFmpeg + FFmpeg --> FinalVideo[成品视频 / 素材库] +``` + +真人短剧主流程: + +```mermaid +flowchart LR + Prompt[外部提示词 / 小说章节 / 项目设定] --> Parse[提示词拆镜 / 分镜草稿] + Parse --> Project[创建 Project / Episode / StoryBible] + Project --> Prepare[真人分镜准备\nTextProvider 可选] + Prepare --> Keyframe[关键帧生成\nImageProvider 可选] + Keyframe --> Video[视频片段生成\nVideoProvider 可选] + Video --> Select[每个分镜选择候选片段] + Select --> Render[FFmpeg 合成] + Render --> Works[作品列表 / 素材列表] +``` + +小说主流程: + +```mermaid +flowchart LR + Import[后台导入小说\n粘贴 / 批量 / 文件] --> Split[章节自动切割] + Split --> NovelSource[NovelSource] + Split --> Chapter[NovelChapter] + NovelSource --> Reader[前台小说详情 / 目录 / 阅读器] + Reader --> Progress[阅读进度] + Reader --> Bookmark[书签] + Reader --> Annotation[批注] + NovelSource --> ShortDrama[选择小说生成短剧] ``` ## 2. 核心模块 -### 后端模块 - -| 模块 | 主要职责 | 关键文件 | -|---|---|---| -| Auth / Users | 注册、登录、JWT、RBAC | `backend/src/auth/*`, `backend/src/users/*` | -| Projects | 项目创建、状态、输入/输出模式 | `backend/src/projects/*` | -| Assets / Storage | 上传、下载、私有素材、本地/MinIO 存储 | `backend/src/assets/*` | -| Novels | AI 原创小说、上传小说解析、章节拆分 | `backend/src/novels/*` | -| Story Bibles | 故事圣经生成/确认 | `backend/src/story-bibles/*` | -| Characters | 角色抽取、角色确认、角色锚点、全局角色关联 | `backend/src/characters/*` | -| Memories | 长篇记忆、剧情线 | `backend/src/memories/*` | -| Episodes | 分集计划生成/确认 | `backend/src/episodes/*` | -| Scripts | 单集脚本、分镜脚本 | `backend/src/scripts/*` | -| Images | 分镜图、角色图 mock/Provider 抽象 | `backend/src/images/*` | -| Media | 多角色 TTS、字幕、普通 FFmpeg 合成 | `backend/src/media/*` | -| Live Action | 真人短剧:演员定妆、真人分镜、关键帧、视频片段、质检、合成 | `backend/src/live-action/*` | -| AI Router | 镜头评分、自动选 Provider、fallback、成本判断 | `backend/src/ai-router/*` | -| Providers | Provider 配置、执行、日志、成本、bootstrap | `backend/src/providers/*` | -| Billing | 额度账户、冻结额度、成本预估 | `backend/src/billing/*` | -| Reviews | 文本/视频审核、公开案例授权 | `backend/src/reviews/*` | -| Queues | 任务查询、worker task 入口 | `backend/src/queues/*` | -| Admin | 后台项目、Provider、日志、审计、Router 视图 | `backend/src/admin/*` | -| Common | API 响应、异常、加密、中间件、HTTPS 检查 | `backend/src/common/*` | - -### 前端模块 - -| 模块 | 主要职责 | 关键文件 | -|---|---|---| -| 用户端制作台 | 创建项目、上传/原创、版权、故事、角色、分集、脚本、真人视频、合成、审核、成品 | `user-app/src/pages/index/index.vue` | -| 用户端 API Client | 用户端 REST API 封装、错误文案、上传下载、真人视频接口 | `user-app/src/api/client.ts` | -| 用户端工作流 | 状态标签、进度计算、任务类型显示 | `user-app/src/workflow.ts` | -| 用户端样式 | 深色运营台风格、单列制作流程、响应式 UI | `user-app/src/styles.css` | -| 后台端 | Provider 配置、AI 平台入口、Router 审计、任务审计、项目管理 | `admin/src/App.vue` | - -### Worker 模块 - -`workers/src/main.ts` 已有 BullMQ worker 框架,队列名包含: - -- `novel_queue` -- `story_queue` -- `character_queue` -- `episode_queue` -- `script_queue` -- `storyboard_queue` -- `image_queue` -- `audio_queue` -- `subtitle_queue` -- `video_queue` -- `review_queue` - -当前大量生成动作仍由 API 同步触发并写 `RenderTask`,队列化已有框架和部分后端队列入口,但仍需要继续把重任务全面迁移到 worker。 - -## 3. 数据表关系 - -### 主业务链路 - -```text -users - └─ projects - ├─ novel_sources - │ └─ novel_chapters - ├─ copyright_records - ├─ story_bibles - ├─ world_bibles - ├─ characters - │ ├─ character_images - │ └─ character_memories - ├─ plot_memories - ├─ plot_threads - ├─ episodes - │ ├─ episode_scripts - │ ├─ storyboard_shots - │ │ ├─ shot_images - │ │ └─ video_clips - │ └─ render_tasks - ├─ assets - ├─ content_reviews - ├─ case_showcases - ├─ project_creative_patterns - └─ analytics_events -``` - -### AI/任务/审计链路 - -```text -provider_configs - ├─ provider_logs - └─ render_tasks.provider_id - -render_tasks - ├─ assets(output_asset_id) - ├─ provider_logs(task_id) - └─ quota_logs(task_id) - -operation_logs - └─ target_type + target_id 记录用户/后台/Router 操作轨迹 - -system_configs - └─ ai.router.v1 / security.api_crypto_enabled / current_stage -``` - -### 角色库链路 - -```text -global_characters - └─ global_character_assets - -projects - └─ characters.global_character_id - ├─ character_images - └─ actor_profiles -``` - -### 爆款诊断 / 模式库 - -```text -hit_analysis_cases - └─ hit_analysis_segments - └─ creative_patterns - └─ project_creative_patterns -``` - -### 关键表说明 - -| 表 | 作用 | -|---|---| -| `users` | 用户和管理员账号 | -| `projects` | 项目主表,区分 `ai_original/upload` 和 `image_manga/motion_comic/live_action_ai` | -| `novel_sources`, `novel_chapters` | 原创/上传小说源和章节 | -| `story_bibles`, `world_bibles` | 故事圣经、世界观设定 | -| `characters`, `global_characters` | 项目角色和全局角色库 | -| `character_images`, `actor_profiles` | 角色锚点图、真人演员定妆 | -| `episodes`, `episode_scripts`, `storyboard_shots` | 分集、脚本、分镜 | -| `shot_images` | 分镜图/关键帧 | -| `video_clips` | 真人视频片段 | -| `assets` | 私有/公开素材,包括小说、图片、音频、字幕、视频 | -| `render_tasks` | 生成任务、输入 JSON、成本、错误、输出素材 | -| `provider_configs` | AI Provider 配置 | -| `provider_logs` | Provider 调用日志、请求/响应摘要、成本 | -| `content_reviews` | 文本/视频审核 | -| `quota_accounts`, `quota_logs` | 用户额度 | -| `system_configs` | 系统配置,包括 Router 配置 | -| `operation_logs` | 操作日志和审计时间线 | - -## 4. AI Provider 列表 - -Provider 默认模板来源: - -- `backend/src/providers/provider.types.ts` -- `backend/prisma/seed.ts` -- `backend/prisma/migrations/20260602130000_domestic_video_providers/migration.sql` - -### Mock Provider - -| Provider Code | 类型 | -|---|---| -| `mock-text` | TextProvider | -| `mock-novel` | NovelProvider | -| `mock-image` | ImageProvider | -| `mock-video` | VideoProvider | -| `mock-voice` | VoiceProvider | -| `mock-lipsync` | LipSyncProvider,默认禁用 | -| `mock-moderation` | ModerationProvider | -| `mock-qc` | QualityCheckProvider | -| `mock-file-parse` | FileParseProvider | -| `mock-embedding` | EmbeddingProvider | - -### OpenAI - -| Provider Code | 类型 | 默认模型 | -|---|---|---| -| `openai-responses-text` | TextProvider | `gpt-5.5` | -| `openai-responses-novel` | NovelProvider | `gpt-5.5` | -| `openai-moderation` | ModerationProvider | `omni-moderation-latest` | -| `openai-embedding` | EmbeddingProvider | `text-embedding-3-small` | -| `openai-image` | ImageProvider | `gpt-image-2` | -| `openai-video` | VideoProvider | `sora-2` | -| `openai-tts` | VoiceProvider | `gpt-4o-mini-tts` | - -### 视频 Provider - -| Provider Code | 平台/模型 | 默认状态 | -|---|---|---| -| `minimax_hailuo_23_fast` | MiniMax Hailuo 2.3 Fast 图生视频 | 默认禁用,当前服务器已启用过 | -| `minimax_hailuo_23` | MiniMax Hailuo 2.3 图生视频 | 默认禁用 | -| `alibaba_wan26_i2v_flash` | 阿里 Wan2.6 I2V Flash | 默认禁用 | -| `alibaba_wan26_i2v` | 阿里 Wan2.6 I2V 标准 | 默认禁用 | -| `vidu_q3_turbo_reference` | Vidu Q3 Turbo 参考图生视频 | 默认禁用 | -| `vidu_q3_pro` | Vidu Q3 Pro 参考图生视频 | 默认禁用 | -| `jimeng_seedance` | 即梦/Seedance 图生视频 | 默认禁用 | -| `runway-image-to-video` | Runway Image-to-Video | 默认禁用 | -| `kling-image-to-video` | Kling Image-to-Video | 默认禁用 | -| `google-veo-video` | Google Veo Video | 默认禁用 | -| `replicate-video` | Replicate Video | 默认禁用 | -| `fal-video` | fal.ai Video | 默认禁用 | -| `luma-ray-video` | Luma Ray Video | 默认禁用 | -| `pika-video` | Pika Video | 默认禁用 | - -### 文本/小说 Provider - -| Provider Code | 平台 | -|---|---| -| `google-gemini-text`, `google-gemini-novel` | Google Gemini | -| `anthropic-claude-text`, `anthropic-claude-novel` | Anthropic Claude | -| `deepseek-text`, `deepseek-novel` | DeepSeek | -| `qwen-text`, `qwen-novel` | Alibaba Qwen | -| `kimi-text`, `kimi-novel` | Moonshot Kimi | -| `zhipu-glm-text`, `zhipu-glm-novel` | 智谱 GLM | -| `baidu-qianfan-text` | 百度千帆 | -| `tencent-hunyuan-text` | 腾讯混元 | -| `iflytek-spark-text` | 讯飞星火 | -| `volcengine-doubao-text`, `volcengine-doubao-novel` | 火山/豆包 | -| `minimax-text` | MiniMax | -| `baichuan-text` | 百川 | -| `stepfun-text` | 阶跃星辰 | -| `sensenova-text` | 商汤日日新 | -| `ai360-text` | 360 AI | -| `mistral-text` | Mistral | -| `cohere-command-text` | Cohere | -| `xai-grok-text` | xAI Grok | -| `openrouter-text` | OpenRouter | -| `together-llama-text` | Together | -| `fireworks-llama-text` | Fireworks | -| `perplexity-sonar-text` | Perplexity | -| `azure-openai-text` | Azure OpenAI | -| `aws-bedrock-openai-compatible-text` | AWS Bedrock OpenAI-compatible | - -### 图片 Provider - -| Provider Code | 平台 | -|---|---| -| `google-gemini-image` | Google Gemini Image | -| `stability-image` | Stability AI | -| `replicate-flux-image` | Replicate Flux | -| `fal-flux-image` | fal.ai Flux | -| `ideogram-image` | Ideogram | -| `leonardo-image` | Leonardo | -| `openai-image` | OpenAI Image | -| `mock-image` | Mock | - -### 语音 Provider - -| Provider Code | 平台 | -|---|---| -| `openai-tts` | OpenAI TTS | -| `elevenlabs-tts` | ElevenLabs | -| `minimax-tts` | MiniMax TTS | -| `volcengine-tts` | 火山 TTS | -| `mock-voice` | Mock | - -### Lip Sync Provider - -| Provider Code | 状态 | -|---|---| -| `mock-lipsync` | Mock,占位 | -| `minimax-lipsync` | 占位,需确认公开 API | -| `alibaba-videoretalk-lipsync` | 阿里 VideoRetalk,要求公网 `video_url/audio_url` | -| `heygen-lipsync` | 默认禁用 | -| `sync-labs-lipsync` | 默认禁用 | -| `fal-veed-lipsync` | 默认禁用 | -| `volcengine-doubao-lipsync` | 占位,需确认公开 API | -| `generic-lipsync` | 通用适配器 | - -## 5. Router 逻辑 - -实现文件: - -- `backend/src/ai-router/ai-router.service.ts` -- `backend/src/ai-router/ai-router.types.ts` -- 真人视频调用入口:`backend/src/live-action/live-action.service.ts` - -### 默认配置 - -`system_configs.config_key = ai.router.v1` - -```json -{ - "version": 1, - "enabled": true, - "default_language": "zh-CN", - "daily_budget": 500, - "live_action_video": { - "zh-CN": { - "thresholds": { - "premium_importance_gt": 7, - "premium_action_gt": 5 - }, - "normal": { - "provider_code": "minimax_hailuo_23_fast", - "fallback_chain": ["minimax_hailuo_23_fast", "jimeng_seedance", "mock-video"] - }, - "premium": { - "provider_code": "kling-image-to-video", - "fallback_chain": ["kling-image-to-video", "minimax_hailuo_23_fast", "jimeng_seedance", "mock-video"] - } - } - } -} -``` - -### 评分字段 - -`StoryboardShot` 上已落库: - -- `scene_type` -- `importance_score` -- `emotion_score` -- `action_score` -- `route_tier` - -### 决策流程 - -```text -StoryboardShot - -> scoreLiveActionShot() - scene_type / importance / emotion / action / route_tier - -> resolveLiveActionVideoRoute() - 如果后台传 provider_code 且允许 override:走人工 Provider - 否则读取 ai.router.v1 - -> fallback_chain - normal: Hailuo -> Jimeng -> Mock - premium: Kling -> Hailuo -> Jimeng -> Mock - -> Provider 可用性检查 - provider 存在 - is_enabled=true - 单片段成本不超过 max_cost_per_clip - 当日预算不超过 daily_budget - -> 输出 AiRouteDecision - provider_code - fallback_chain - candidates - estimated_cost - decision_reason - scores -``` - -### 已实现能力 - -- 没传 `provider_code` 时自动走 Router。 -- 后台/管理员可人工 override。 -- 任务 `input_json` 记录 `router_decision`。 -- 支持 fallback 链、禁用 Provider 跳过、成本上限跳过、日预算跳过。 -- Router 审计数据可从 `RenderTask.input_json`、`ProviderLog`、`OperationLog` 读取。 - -### 待优化 - -- Router 当前重点服务真人视频片段,文本/图片/TTS 多 Provider Router 仍可继续统一。 -- 日预算目前基于 ProviderLog 聚合,缺少更强的账户级预算锁。 -- Premium 阈值配置存在,但 `routeTierForScores` 的细节还可进一步后台可视化。 -- Router 决策的人工可解释 UI 还可继续增强。 - -## 6. Prompt Builder 实现情况 - -实现文件: - -- `backend/src/live-action/prompt-builder.service.ts` -- 调用点:`backend/src/live-action/live-action.service.ts` - -### 已实现 - -`LiveActionPromptBuilderService` 已实现真人视频 Prompt Engine V1: - -- Provider profile: - - `generic` - - `hailuo` - - `kling` - - `mock` -- 结构化输入: - - 项目/集/镜头编号 - - Provider Code - - scene type / route tier - - duration - - characters - - actor consistency rules - - location - - action - - visual description - - camera motion / camera instruction - - performance instruction - - dialogue / narration - - effect type - - scores - - lip-sync policy - - director plan -- 输出: - - `prompt` - - `negative_prompt` - - `components` - - `prompt_version` - - `provider_profile` - -### 场景模板 - -Prompt Builder 内置 scene template,用于决定: - -- camera shot -- camera move -- lighting -- performance -- vfx cue -- sound cue -- negative motion - -### Hailuo 专项 - -Hailuo profile 会输出中文短剧 Prompt,包含: - -- 真人短剧竖屏 9:16 -- 项目/集/镜头信息 -- 场景、人物、演员一致性 -- 主动作 -- Motion Director -- Director Plan -- 镜头、表演、光线、特效、后期音效提示 -- lip-sync fallback 约束 -- “只完成一个主要动作,不要突然切场景” -- Hailuo 负面词中包含避免“一条 clip 塞长多动作” - -### Lip Sync 策略 - -当没有稳定 lip-sync Provider 或镜头不适合正脸说话时,策略会落为: - -- `post_tts_subtitle_light_mouth` - -Prompt 会要求: - -- 避免正脸嘴部特写 -- 台词后期由 TTS 和字幕处理 -- 演员只做轻微口型/反应/表情 - -### Motion Director / Director Plan - -Live Action Service 会补充: - -- `director_plan` - - scene group - - shot role - - shot size - - continuity in/out - - edit intent - - sound bridge -- `motion_director` - - time beats - - camera rhythm - - vfx timing - - sound hits - - negative motion - -### 待优化 - -- Prompt Library/题材套路库已经有数据表,但与 Prompt Builder 的深度联动还不完整。 -- 爆款拉片沉淀的 `creative_patterns` 还未全面驱动真人视频 Prompt。 -- 专业音效/BGM 的剧情化选择已部分在 Live Action 后期里实现,但还缺可运营素材库。 -- 多 Provider 差异化 Prompt 可继续细分,如 Kling/Runway/Veo 专用结构。 - -## 7. Character Library 实现情况 - -实现文件: - -- `backend/src/characters/characters.service.ts` -- `backend/src/live-action/live-action.service.ts` -- 数据表:`characters`, `global_characters`, `global_character_assets`, `character_images`, `actor_profiles` - -### 项目角色 - -已实现: - -- 从故事圣经和章节抽取角色。 -- 角色字段包含: - - name / aliases / role_type - - gender / age / identity - - appearance / face / hair / eye / body - - costume / props - - personality / speech / relationship / arc - - negative_rules - - anchor_asset_id - - voice provider/model/id/style - - performance_style -- 角色确认后锁定关键字段。 -- 锁定后只允许有限字段变更,避免破坏一致性。 -- 角色锚点图生成、重生成、切换锚点。 - -### 全局角色库 - -已实现数据结构: - -- `global_characters` -- `global_character_assets` -- 项目角色可通过 `global_character_id` 关联全局角色。 - -当前定位: - -- 已具备全局角色库基础表和后台入口。 -- 更偏“生产系统预留 + 初步可用”,还不是完整运营级角色资产市场。 - -### 真人 Actor Profile - -Live Action 已实现: - -- `actor_profiles` -- 每个项目角色生成演员定妆。 -- 字段包含: - - actor_desc - - appearance_rules - - wardrobe_rules - - performance_style - - voice_style - - reference_asset_ids - - anchor_asset_id - -### Actor Lock V1 - -已实现: - -- 生成每个真人视频镜头时,只把该镜头出现的角色 ActorProfile 注入 Provider Prompt。 -- 记录: - - `actor_lock.character_names` - - `actor_lock.actor_hints` - - `reference_asset_ids` - - `provider_character_reference_enabled` - - `actor_lock.audit` -- 防止“全项目角色混入一个镜头”。 -- 任务 `input_json.actor_lock` 可审计。 - -### 待优化 - -- 多角色同镜头的脸部一致性仍依赖 Provider 能力。 -- 角色参考图/定妆图生成质量还需要更多真实 Provider 对比。 -- 全局角色复用、授权、商用范围、声音样本绑定仍需完善运营闭环。 - -## 8. FFmpeg 流程 - -### 普通漫剧/图片视频流程 - -实现文件: - -- `backend/src/media/media.service.ts` - -流程: - -```text -confirmed storyboard shots - -> shot_images - -> generateEpisodeAudio() - 多角色 TTS - 同文本 + 同 voice 缓存复用 - segment files 落盘 - timeline warnings - -> generateEpisodeSubtitle() - dialogue-level SRT - shot-level SRT 可选 - -> renderEpisodeVideo() - prefer_ffmpeg=true 时走本地 FFmpeg - 图片转视频 - 音频/字幕合成 - 输出 MP4 Asset -``` - -FFmpeg 能力: - -- 检查 ffmpeg 是否存在。 -- 分镜图写入临时目录。 -- 音频/字幕素材写入临时目录。 -- SRT 转 ASS 字幕。 -- 图片生成视频轨。 -- 音频混合。 -- 输出 `video/mp4`。 -- 失败时写 `RenderTask.error_code/error_message`。 - -### 真人短剧流程 - -实现文件: - -- `backend/src/live-action/live-action.service.ts` - -片段生成: - -```text -StoryboardShot + keyframe - -> Router 选 Provider - -> Prompt Builder - -> Provider 生成 6s/10s clip - -> action_beat_mode 可拆成 2-3 段 - -> 多段之间可提取上一段末帧作为下一段关键帧 - -> concat + trim 到目标时长 - -> VideoClip + Asset -``` - -整集合成: - -```text -VideoClip assets - -> normalizeLiveActionClipForRender() - ffprobe 探测源时长 - 超出目标时长则自动裁切 - 记录 clip_normalization - -> concatVideoClips() - -> prepareLiveActionPostProductionAssets() - TTS 对白 - SRT/ASS 字幕 - BGM - SFX - 可选 lip-sync - -> finalizeLiveActionRenderWithPostProduction() - 视频调色/暗角/字幕 - dialogue + bgm + sfx 混音 - loudnorm / limiter - 输出 episode-final.mp4 -``` - -真人后期 FFmpeg 参数特点: - -- `libx264` -- `-preset veryfast` -- `-crf 20` -- `yuv420p` -- `+faststart` -- 音频 `aac 160k 44100 stereo` -- BGM fade in/out -- SFX limiter -- dialogue loudnorm -- 混音 `amix normalize=0` -- 字幕走 ASS/FFmpeg subtitles filter - -### 已记录审计 - -- `renderTask.input_json.clip_normalization` -- 是否裁切、源时长、目标时长、最终时长、裁切策略、trim start。 -- 最终成片状态: - - 全 mock clip -> asset `mock` - - 任一真实 clip -> asset `active` - -## 9. 当前已完成功能 - -### 系统 A 基础 MVP - -- 用户注册登录。 -- 项目创建。 -- AI 原创小说 mock 流程。 -- 上传小说、解析、章节拆分。 -- 版权确认。 -- 故事圣经生成/确认。 -- 角色抽取/确认。 -- 角色锚点图。 -- 长篇记忆。 -- 分集计划。 -- 单集脚本。 -- 分镜脚本。 -- 图片生成 mock/Provider 抽象。 -- 多角色 TTS。 -- 字幕生成。 -- FFmpeg 合成 MP4。 -- 用户端查看进度、预览、下载。 -- 后台项目管理、任务管理、Provider 管理。 -- 内容审核。 -- 公开案例授权。 -- 额度账户和冻结额度。 - -### AI Provider - -- Provider 抽象层。 -- Mock Provider。 -- OpenAI Provider 模板。 -- 国内外主流文本/图片/视频/TTS/lip-sync Provider 模板。 -- Provider 日志。 -- Provider 成本摘要。 -- 后台统一保存同公司 Key 的体验优化。 -- MiniMax TTS 实测修复: - - voice_id 映射 - - 空音频错误识别 - - Provider 拒绝错误透出。 - -### 真人短剧 - -- `live_action_ai` 输出模式。 -- 演员定妆 ActorProfile。 -- 真人分镜改写。 -- 关键帧生成/上传绑定。 -- 视频片段生成: - - Hailuo / Kling / Jimeng / Mock 路由 - - preflight - - 成本预估 - - 单片段成本上限 - - 真实费用确认 - - action beat 拆段 - - candidate_count 支持 -- Router 质检闭环: - - 质检 - - 低分自动重试 - - fallback Provider - - manual_required - - 人工通过/驳回 -- 片段重试。 -- 小样测试台。 -- 真人整集合成。 -- TTS/BGM/SFX/字幕后期合成。 -- clip normalization 自动裁切。 -- actor lock 审计。 - -### 前端/后台体验 - -- 用户端制作页已改为单列纵向流程。 -- 每步完成后自动滚动到下一步。 -- 用户端真人视频面板已同步 action beat 控制。 -- 后台深色运营台风格。 -- 后台 AI 平台入口和配置。 -- Router/质检审计视图和操作闭环已有实现。 - -## 10. 待优化功能 - -### 生产级队列化 - -- 当前 BullMQ/worker 框架已存在,但重型生成流程仍有大量 API 同步执行。 -- 需要把: - - 重新质检 - - 自动修复 - - 指定 Provider 重试 - - 真人视频生成 - - FFmpeg 合成 - 全部任务化,形成可暂停、恢复、重跑、精确计费的队列闭环。 - -### 真人视频质量 - -- Hailuo 对复杂动作、翻滚、结印、法相天地等高复杂特效表现不稳定。 -- 需要更多 Provider 准入测试: - - Kling - - Runway - - Vidu - - Veo - - 阿里 Wan - - 即梦/Seedance -- 都市短剧可继续用 Hailuo 主力;修仙/特效/复杂动作需要更强模型。 - -### Lip Sync - -- 已有 lip-sync 抽象和多个 Provider 占位。 -- 稳定公开 API 仍需逐个平台实测。 -- MiniMax/豆包 lip-sync 目前标为占位,不能假定已稳定可用。 -- 阿里 VideoRetalk 需要公网 `video_url/audio_url`,素材公网桥接已设计但仍需生产验证。 - -### Prompt Engine - -- 真人 Prompt Builder V1 已落地。 -- Prompt Library / CreativePattern 与实际脚本、分镜、真人 Prompt 的联动还需加强。 -- 需要题材模板: - - 都市逆袭 - - 霸总打脸 - - 修仙法相 - - 聊斋志异 - - 次元壁 -- 需要把拉片分析的镜头语言、音效、BGM、剪辑节奏转为可复用模板。 - -### Character Library - -- 全局角色表已存在。 -- 还需要: - - 前台化角色资产库 - - 商用授权状态 - - 角色复用统计 - - voice sample 管理 - - 多项目角色一致性验证 - -### 成本/ROI - -- Provider 成本估算和日志已有。 -- 还需接真实账单回填、播放数据、完播率、收益,形成 ROI: - - 每题材 ROI - - 每 Provider 成本/成功率 - - 每镜头类型失败率 - - 每集成本和收益 - -### 数据清理/生产隔离 - -- 当前服务器数据库包含大量测试项目、任务、素材和 ProviderLog。 -- Git 只提交源码,不提交数据库和 storage。 -- 上线前建议: - - 分测试库/生产库 - - 测试素材定期清理 - - ProviderLog 按时间归档 - - storage 生命周期策略 - -## 11. 关键文件路径 - -### 根目录 - -| 文件 | 作用 | -|---|---| -| `README.md` | 项目说明 | -| `OPERATION_GUIDE.md` | 操作说明 | -| `AGENTS.md` | Codex 开发约束 | -| `.env.example` | 环境变量模板 | -| `.gitignore` | Git 忽略规则 | -| `CODEX_PROGRESS.md` | 阶段进度记录 | -| `AI_VIDEO_TEST_LESSONS.md` | 真人视频测试经验记录 | -| `CURRENT_ARCHITECTURE.md` | 当前架构文档 | - -### 后端 - -| 文件 | 作用 | -|---|---| -| `backend/src/app.module.ts` | Nest 模块总入口 | -| `backend/src/main.ts` | 后端启动入口 | -| `backend/prisma/schema.prisma` | 数据库 schema | -| `backend/prisma/migrations/*/migration.sql` | 数据库迁移 | -| `backend/prisma/seed.ts` | 初始 mock Provider、Router、管理员 | -| `backend/src/providers/provider.types.ts` | Provider 默认模板和成本摘要 | -| `backend/src/providers/providers.service.ts` | Provider bootstrap、执行、日志、配置 | -| `backend/src/ai-router/ai-router.service.ts` | Router 决策 | -| `backend/src/ai-router/ai-router.types.ts` | Router 默认配置和类型 | -| `backend/src/live-action/live-action.service.ts` | 真人短剧核心流程 | -| `backend/src/live-action/prompt-builder.service.ts` | 真人 Prompt Builder | -| `backend/src/media/media.service.ts` | TTS、字幕、普通 FFmpeg 合成 | -| `backend/src/assets/storage.service.ts` | 存储抽象 | -| `backend/src/admin/admin.service.ts` | 后台审计和管理 | - -### 用户端 - -| 文件 | 作用 | -|---|---| -| `user-app/src/pages/index/index.vue` | 用户端主制作台 | -| `user-app/src/api/client.ts` | 用户端 API 封装 | -| `user-app/src/workflow.ts` | 工作流标签和进度 | -| `user-app/src/styles.css` | 用户端样式 | - -### 后台端 - -| 文件 | 作用 | -|---|---| -| `admin/src/App.vue` | 后台管理主界面 | -| `admin/src/api/client.ts` | 后台 API 封装 | -| `admin/src/styles.css` | 后台样式 | +### 后端 Backend + +入口: + +- `backend/src/main.ts`:全局 `/api` 前缀、CORS、请求体大小、全局拦截器、异常过滤器。 +- `backend/src/app.module.ts`:注册全部业务模块。 + +核心模块: + +- `AuthModule`:登录、JWT、权限守卫。 +- `UsersModule`:用户资料和账号信息。 +- `BillingModule`:订单、额度、Provider 消耗记录。 +- `ProjectsModule`:项目创建、提示词直出分镜、作品库、小说库前台接口。 +- `AssetsModule`:文件上传、素材存储、临时公网访问。 +- `NovelsModule`:小说导入、解析、章节管理、原创小说工具。 +- `StoryBiblesModule`:故事圣经、世界观设定。 +- `CharactersModule`:项目角色库、我的真人脸包、角色定妆版本、状态变体。 +- `MemoriesModule`:剧情记忆、连续性。 +- `EpisodesModule`:分集。 +- `ScriptsModule`:脚本、分镜。 +- `ImagesModule`:角色图、关键帧、锚点图生成。 +- `LiveActionModule`:真人短剧全流程。 +- `MediaModule`:传统音频、字幕、视频合成。 +- `ProvidersModule`:AI Provider 注册、调用、配置、日志、成本。 +- `QueuesModule`:RenderTask、BullMQ 队列、内部 Worker 执行入口。 +- `ReviewsModule`:内容审核、返修、案例展示。 +- `AdminModule`:后台用户、项目、素材、小说、Provider、Router、运营数据。 + +### 前端 User App + +- 技术栈:Vue 3 + Vite。 +- 主要页面:`user-app/src/pages/index/index.vue`。 +- 主要能力:作品列表、视频/小说/素材 Tab、提示词直出分镜、真人短剧流程、模型选择、素材预览、小说阅读器、我的真人脸包。 + +### 后台 Admin + +- 技术栈:Vue 3 + Vite。 +- 主要页面:`admin/src/App.vue`。 +- 主要能力:Provider 配置、模型开关、成本规则、小说管理、章节管理、素材管理、用户项目管理、运营看板。 ### Worker -| 文件 | 作用 | -|---|---| -| `workers/src/main.ts` | BullMQ worker 入口 | +- `workers/src/main.ts`:BullMQ Worker 进程。 +- Worker 不直接写复杂业务,主要从队列拿 `RenderTask`,再调用后端内部接口执行。 -### 运维 +## 3. 数据表关系 -| 文件 | 作用 | -|---|---| -| `deploy/docker-compose.dev.yml` | 开发依赖服务 | -| `deploy/nginx.https.example.conf` | Nginx HTTPS 示例 | -| `deploy/README.md` | 部署说明 | +当前 Prisma Schema 以 `*_id` 字段做逻辑关联,很多模型没有声明 Prisma `@relation`。下面是业务关系视图,不代表数据库里已经全部建外键约束。 -## 12. 当前架构判断 +```mermaid +erDiagram + User ||--o{ Project : owns + User ||--o{ NovelReadingProgress : reads + User ||--o{ NovelBookmark : bookmarks + User ||--o{ NovelAnnotation : annotates + User ||--o{ GlobalCharacter : owns -当前系统已经从“普通 AI 漫剧 MVP”扩展到“AI 真人短剧生产流水线雏形”: + Project ||--o{ StoryBible : has + Project ||--o{ WorldBible : has + Project ||--o{ Character : has + Project ||--o{ Episode : has + Project ||--o{ Asset : has + Project ||--o{ CopyrightRecord : has + Project ||--o{ RenderTask : has + Project ||--o{ ActorProfile : has -- 基础项目/小说/故事/角色/分集/脚本/分镜/合成已跑通。 -- Provider 抽象、Router、质检、成本、审计已经具备生产系统骨架。 -- 真人短剧的关键难点已经开始落地: - - 角色定妆 - - Actor Lock - - 专业 Prompt Builder - - 真实 Provider 小样 - - 片段质检 - - 后期音频/BGM/SFX/字幕 - - FFmpeg 裁切/拼接/混音 + NovelSource ||--o{ NovelChapter : has + NovelSource ||--o{ NovelReadingProgress : has + NovelSource ||--o{ NovelBookmark : has + NovelSource ||--o{ NovelAnnotation : has -下一阶段最重要的不是继续堆 Provider,而是把真人视频生产质量和任务稳定性打磨成可重复流水线: + Episode ||--o{ EpisodeScript : has + Episode ||--o{ StoryboardShot : has + Episode ||--o{ VideoClip : has + + StoryboardShot ||--o{ ShotImage : has + StoryboardShot ||--o{ VideoClip : candidates + + Character ||--o{ CharacterImage : has + Character ||--o{ CharacterMemory : has + Character ||--o{ CharacterDesignVersion : has + Character ||--o{ CharacterState : has + + GlobalCharacter ||--o{ GlobalCharacterAsset : has + GlobalCharacter ||--o{ Character : binds + + ProviderConfig ||--o{ ProviderLog : logs + ProviderConfig ||--o{ RenderTask : used_by + Asset ||--o{ VideoClip : stores +``` + +核心表分组: + +| 分组 | 表 | +| --- | --- | +| 用户 / 项目 | `User`, `Project`, `CopyrightRecord` | +| 小说 / 阅读 | `NovelSource`, `NovelChapter`, `NovelReadingProgress`, `NovelBookmark`, `NovelAnnotation` | +| 故事世界 | `StoryBible`, `WorldBible` | +| 角色 | `Character`, `GlobalCharacter`, `GlobalCharacterAsset`, `CharacterImage`, `CharacterMemory`, `CharacterDesignVersion`, `CharacterState`, `ActorProfile` | +| 分集 / 分镜 | `Episode`, `EpisodeScript`, `StoryboardShot`, `ShotImage` | +| 视频 / 素材 | `VideoClip`, `Asset`, `RenderTask` | +| Provider / 成本 | `ProviderConfig`, `ProviderLog`, `Order`, `QuotaAccount`, `QuotaLog` | +| 审核 / 运营 | `RevisionRequest`, `ContentReview`, `CaseShowcase`, `OperationLog`, `SystemConfig` | +| 爆款分析 | `HitAnalysisCase`, `HitAnalysisSegment`, `CreativePattern`, `ProjectCreativePattern`, `AnalyticsEvent` | + +重要唯一约束和索引: + +- `NovelChapter`:`novel_source_id + chapter_no` 唯一,支持章节分页和卷号索引。 +- `NovelReadingProgress`:`user_id + novel_source_id` 唯一。 +- `CharacterDesignVersion`:`character_id + version_no` 唯一。 +- `CharacterState`:`character_id + state_code` 唯一。 +- `Episode`:`project_id + episode_no` 唯一。 +- `StoryboardShot`:`episode_id + shot_no` 唯一。 +- `ActorProfile`:`project_id + character_id` 唯一。 +- `ProviderConfig`:`provider_type + provider_code` 唯一。 +- `RenderTask`:`idempotency_key` 唯一。 +- `ProjectCreativePattern`:`project_id + creative_pattern_id` 唯一。 + +## 4. AI Provider 列表 + +Provider 配置来自 `ProviderConfig` 表和初始化配置。状态按当前扫描结果记录。 + +### Provider 类型统计 + +| 类型 | 总数 | 启用 | +| --- | ---: | ---: | +| `TextProvider` | 35 | 26 | +| `NovelProvider` | 15 | 9 | +| `ImageProvider` | 11 | 5 | +| `VideoProvider` | 21 | 10 | +| `VoiceProvider` | 5 | 2 | +| `LipSyncProvider` | 8 | 0 | +| `EmbeddingProvider` | 2 | 2 | +| `ModerationProvider` | 2 | 2 | +| `FileParseProvider` | 1 | 1 | +| `QualityCheckProvider` | 1 | 1 | + +### TextProvider + +已启用: + +| Provider | 显示名 | 模型 | +| --- | --- | --- | +| `volcengine-doubao-seed20-pro-text` | 豆包文本 2.0 Pro | `doubao-seed-2-0-pro-260215` | +| `volcengine-doubao-seed20-lite-text` | 豆包文本 2.0 Lite | `doubao-seed-2-0-lite-260215` | +| `volcengine-doubao-seed20-mini-text` | 豆包文本 2.0 Mini | 当前配置 | +| `volcengine-doubao-text` | 豆包文本 | `doubao-seed-1-6-250615` | +| `deepseek-text` | DeepSeek | `deepseek-chat` | +| `qwen-text` | 通义千问 | `qwen-plus` | +| `kimi-text` | Kimi | `kimi-latest` | +| `zhipu-glm-text` | 智谱 GLM | `glm-4.5` | +| `baidu-qianfan-text` | 百度千帆 | 当前配置 | +| `tencent-hunyuan-text` | 腾讯混元 | 当前配置 | +| `iflytek-spark-text` | 讯飞星火 | 当前配置 | +| `minimax-text` | MiniMax 文本 | 当前配置 | +| `baichuan-text` | 百川 | 当前配置 | +| `stepfun-text` | 阶跃星辰 | 当前配置 | +| `sensenova-text` | 商汤日日新 | 当前配置 | +| `ai360-text` | 360 智脑 | 当前配置 | +| `mistral-text` | Mistral | 当前配置 | +| `xai-grok-text` | xAI Grok | 当前配置 | +| `openrouter-text` | OpenRouter | 当前配置 | +| `together-llama-text` | Together Llama | 当前配置 | +| `fireworks-llama-text` | Fireworks Llama | 当前配置 | +| `perplexity-sonar-text` | Perplexity Sonar | 当前配置 | +| `azure-openai-text` | Azure OpenAI | 当前配置 | +| `aws-bedrock-openai-compatible-text` | AWS Bedrock OpenAI Compatible | 当前配置 | +| `openai-responses-text` | OpenAI Responses | `gpt-5.5` | +| `mock-text` | Mock Text | Mock | + +未启用: + +- `openai-gpt54-text` +- `openai-gpt54-mini-text` +- `openai-gpt54-nano-text` +- `openai-gpt5-text` +- `openai-gpt41-text` +- `openai-gpt41-mini-text` +- `google-gemini-text` +- `anthropic-claude-text` +- `cohere-command-text` + +### NovelProvider + +已启用: + +| Provider | 显示名 | 模型 | +| --- | --- | --- | +| `volcengine-doubao-seed20-pro-novel` | 豆包小说 2.0 Pro | `doubao-seed-2-0-pro-260215` | +| `volcengine-doubao-seed20-lite-novel` | 豆包小说 2.0 Lite | `doubao-seed-2-0-lite-260215` | +| `volcengine-doubao-novel` | 豆包小说 | `doubao-seed-1-6-250615` | +| `deepseek-novel` | DeepSeek 小说 | `deepseek-chat` | +| `qwen-novel` | 通义小说 | `qwen-plus` | +| `kimi-novel` | Kimi 小说 | `kimi-latest` | +| `zhipu-glm-novel` | 智谱小说 | `glm-4.5` | +| `openai-responses-novel` | OpenAI Responses 小说 | `gpt-5.5` | +| `mock-novel` | Mock Novel | Mock | + +未启用: + +- `openai-gpt54-novel` +- `openai-gpt54-mini-novel` +- `openai-gpt5-novel` +- `openai-gpt41-novel` +- `google-gemini-novel` +- `anthropic-claude-novel` + +### ImageProvider + +已启用: + +| Provider | 显示名 | 模型 | +| --- | --- | --- | +| `volcengine-seedream-50-image` | 豆包 Seedream 5.0 图片 | `doubao-seedream-5-0-260128` | +| `volcengine-seedream-50-lite-image` | 豆包 Seedream 5.0 Lite 图片 | `doubao-seedream-5-0-lite-260128` | +| `volcengine-seedream-45-image` | 豆包 Seedream 4.5 图片 | `doubao-seedream-4-5-251128` | +| `openai-image` | OpenAI Image | `gpt-image-2` | +| `mock-image` | Mock Image | Mock | + +未启用: + +- `google-gemini-image` +- `stability-image` +- `replicate-flux-image` +- `fal-flux-image` +- `ideogram-image` +- `leonardo-image` + +### VideoProvider + +已启用: + +| Provider | 显示名 | 模型 | +| --- | --- | --- | +| `volcengine_seedance_20_fast` | 豆包 Seedance 2.0 Fast 视频 | `doubao-seedance-2-0-fast-260128` | +| `volcengine_seedance_20` | 豆包 Seedance 2.0 视频 | `doubao-seedance-2-0-260128` | +| `jimeng_seedance` | 豆包 Seedance 1.5 Pro 视频 | `doubao-seedance-1-5-pro-251215` | +| `kling-image-to-video` | 可灵图生视频 | `kling-v3` | +| `kling-21-image-to-video` | 可灵 2.1 图生视频 | `kling-v2-1` | +| `kling-21-master-image-to-video` | 可灵 2.1 Master 图生视频 | `kling-v2-1-master` | +| `minimax_hailuo_23_fast` | 海螺 2.3 Fast | `MiniMax-Hailuo-2.3-Fast` | +| `minimax_hailuo_23` | 海螺 2.3 | `MiniMax-Hailuo-2.3` | +| `openai-video` | OpenAI Sora | `sora-2` | +| `mock-video` | Mock Video | Mock | + +未启用: + +- `alibaba_wan26_i2v_flash` +- `alibaba_wan26_i2v` +- `vidu_q3_turbo_reference` +- `vidu_q3_pro` +- `openai-sora-2-pro-video` +- `runway-image-to-video` +- `google-veo-video` +- `replicate-video` +- `fal-video` +- `luma-ray-video` +- `pika-video` + +### VoiceProvider + +已启用: + +- `openai-tts`:`gpt-4o-mini-tts` +- `mock-voice` + +未启用: + +- `elevenlabs-tts` +- `minimax-tts` +- `volcengine-tts` + +### LipSyncProvider + +当前全部未启用: + +- `mock-lipsync` +- `minimax-lipsync` +- `alibaba-videoretalk-lipsync` +- `heygen-lipsync` +- `sync-labs-lipsync` +- `fal-veed-lipsync` +- `volcengine-doubao-lipsync` +- `generic-lipsync` + +### 其他 Provider + +已启用: + +- `openai-embedding` +- `mock-embedding` +- `mock-file-parse` +- `openai-moderation` +- `mock-moderation` +- `mock-quality-check` + +## 5. Router 逻辑 + +Router 主要服务真人视频生成,核心文件: + +- `backend/src/ai-router/ai-router.service.ts` +- `backend/src/ai-router/ai-router.types.ts` + +默认配置: + +- Router 版本:`ai.router.v1` +- 默认语言:`zh-CN` +- 每日预算:`500` +- 普通路线默认 Provider:`minimax_hailuo_23_fast` +- 高价值路线默认 Provider:`kling-image-to-video` +- 普通 fallback:`minimax_hailuo_23_fast` → `volcengine_seedance_20_fast` → `volcengine_seedance_20` → `jimeng_seedance` → `mock-video` +- 高价值 fallback:`kling-image-to-video` → `volcengine_seedance_20` → `minimax_hailuo_23_fast` → `volcengine_seedance_20_fast` → `jimeng_seedance` → `mock-video` + +决策步骤: + +1. 如果前端手动选择了视频模型,并且 Router 允许手动覆盖,优先使用手动 Provider。 +2. 如果手动选择了模型,但系统不允许覆盖,直接返回错误,避免假装生效。 +3. 没有手动选择时,读取系统 Router 配置。 +4. 根据分镜文本推断 `scene_type`,例如对话、冲突、揭露、武打、玄幻变身等。 +5. 对分镜打分:重要性、情绪强度、动作强度。 +6. 如果重要性或动作强度较高,进入 `premium` 路线;否则进入 `normal` 路线。 +7. 根据候选 Provider 顺序逐个检查: + - Provider 是否存在。 + - Provider 是否启用。 + - 单条成本是否超过上限。 + - 当日预算是否超过限制。 + - 时长、分辨率、比例等能力是否可用。 +8. 选中第一个可用 Provider。 +9. 记录 Router 决策、跳过原因、成本估算、Provider 调用日志。 + +当前实现特点: + +- 支持前端手动指定 Provider。 +- 支持按分镜打分自动调度。 +- 支持 Provider fallback。 +- 支持成本估算和日限额拦截。 +- 支持 Provider 不可用时给出明确跳过原因。 +- 支持长时长拆段,避免单个 Provider 不支持当前时长。 + +## 6. Prompt Builder 实现情况 + +核心文件: + +- `backend/src/live-action/prompt-builder.service.ts` +- `backend/src/live-action/prompt-builder.service.spec.ts` + +当前版本: + +- `live-action-prompt-engine-v2` + +Provider Profile: + +- `generic` +- `hailuo` +- `kling` +- `mock` + +场景模板: + +- `dialog` +- `conflict` +- `reveal` +- `engagement_breakup` +- `humiliation` +- `rich_arrival` +- `identity_reveal` +- `bank_balance_reveal` +- `boardroom_face_slap` +- `ceo_entrance` +- `dimensional_break` +- `xianxia_transformation` +- `action` + +Prompt Builder 输出内容: + +- prompt 版本。 +- Provider profile。 +- scene type。 +- template ids。 +- applied rules。 +- route tier。 +- aspect ratio。 +- visual style。 +- characters。 +- actor consistency rules。 +- location。 +- main action。 +- camera shot / camera move / camera tag。 +- performance。 +- lighting。 +- VFX / sound cues。 +- duration。 +- continuity rules。 +- motion director。 +- director plan。 +- lip sync policy。 +- negative prompt。 + +不同 Provider 的处理: + +- 海螺:偏中文真人短剧风格,控制提示词长度,强调镜头、人物、动作和短剧质感。 +- 可灵:偏图生视频、参考图、动作连续性和真实镜头。 +- 通用:偏 photorealistic vertical short drama。 +- Mock:输出结构化 key-value,便于测试。 + +当前使用位置: + +- 真人分镜准备后,视频片段生成前,会根据 Router 决策和 Provider Profile 重新构建最终提交给视频模型的 Prompt。 +- `LiveActionService` 会把分镜、角色锁定信息、导演计划、唇形策略、Provider 能力一起传给 Prompt Builder。 + +待加强点: + +- Prompt Builder 目前主要服务真人视频生成。 +- 小说、故事圣经、角色抽取、分集、脚本、普通分镜等文本步骤虽然已有 Provider 接入点,但还需要统一成更严格的 schema、重试、验证和 Prompt 版本管理。 +- 可继续增加“Prompt 调试面板”,把最终提交给 Provider 的完整参数、参考图、负面词、成本估算展示给前端测试人员。 + +## 7. Character Library 实现情况 + +核心文件: + +- `backend/src/characters/characters.service.ts` +- `backend/src/characters/characters.controller.ts` +- `backend/src/characters/character.dto.ts` +- `backend/src/characters/character.types.ts` +- `backend/src/images/images.service.ts` +- `backend/src/live-action/live-action.service.ts` + +当前角色体系分两层: + +### 我的真人脸包 + +对应表: + +- `GlobalCharacter` +- `GlobalCharacterAsset` + +当前逻辑: + +- 用户可以在“我的”里维护长期可复用的真人脸包。 +- 真人脸包只保存脸部身份参考。 +- 上传时需要本人/授权确认。 +- 支持 1-10 张脸部参考图。 +- 不默认把真人照当作项目角色锚点。 +- 后续项目套用脸包时,需要在项目里生成或选择“仿真定妆锚点图”。 + +这样做的原因: + +- 真人照片只用于锁脸,不应直接作为视频主角图。 +- 不同项目有不同服装、发型、时代、风格,需要在项目内重新定妆。 +- 视频模型对真人照片有限制时,可以把真人脸包作为身份参考,而不是首帧图。 + +### 项目角色库 + +对应表: + +- `Character` +- `CharacterImage` +- `CharacterDesignVersion` +- `CharacterState` +- `ActorProfile` + +当前逻辑: + +- 项目可以从故事圣经、章节、提示词中抽取角色。 +- 角色可以绑定我的真人脸包。 +- 角色可以生成多张定妆图。 +- 可设置角色锚点图。 +- 可保存设计版本。 +- 可保存状态变体,例如不同服装、受伤状态、战斗状态。 +- `ActorProfile` 用于真人短剧阶段锁定角色参考图、锚点、脸包引用。 + +真人视频使用角色时: + +1. 优先使用项目内已确认的定妆锚点图。 +2. 真人脸包作为脸部参考。 +3. 如果 Provider 支持角色参考图,则把参考图传入 Provider。 +4. 如果 Provider 不支持角色参考图,则只在 Prompt 和前置关键帧生成里体现角色一致性。 +5. 对不允许真人图直接生成视频的 Provider,会排除真人脸包作为首帧,避免触发真实人物限制。 + +## 8. FFmpeg 流程 + +相关文件: + +- `backend/src/live-action/live-action.service.ts` +- `backend/src/media/media.service.ts` +- `backend/src/live-action/live-action.types.ts` + +真人短剧 FFmpeg 合成流程: + +```mermaid +flowchart TB + Selected[每个分镜已选择 VideoClip] --> Download[下载/读取素材文件] + Download --> Probe[ffprobe 探测\n时长 / 音轨 / 编码] + Probe --> Normalize[标准化片段\n裁剪 / 补齐 / 缩放 / pad / fps] + Normalize --> Concat[concat demuxer 拼接] + Concat --> VideoTrack[video-track.mp4] + + VideoTrack --> Post[后期处理] + Dialogue[可选后期对白 / TTS] --> Post + BGM[可选 BGM] --> Post + SFX[可选音效] --> Post + Subtitle[可选 ASS 字幕] --> Post + + Post --> Encode[编码输出\nlibx264 / libopenh264 / mpeg4 fallback] + Encode --> Final[最终 MP4] +``` + +标准化动作: + +- 读取源视频。 +- 按分镜时长裁剪或补齐。 +- 统一为竖屏 `720x1280`。 +- `force_original_aspect_ratio=decrease` 后 pad。 +- `setsar=1`。 +- `fps=30`。 +- `format=yuv420p`。 +- 根据配置决定是否保留源音频。 + +拼接动作: + +- 每段生成临时标准化 clip。 +- 写入 `clips.txt`。 +- 使用 FFmpeg concat demuxer 拼接。 +- 输出 `video-track.mp4`。 + +后期动作: + +- 可选后期对白。 +- 可选 BGM。 +- 可选 SFX。 +- 可选字幕。 +- 可选画面 polish filter。 +- 音频使用 `amix`、`loudnorm`、`limiter` 等策略混合。 + +编码器 fallback: + +1. 优先 `libx264`。 +2. 如果环境没有 `libx264`,使用 `libopenh264`。 +3. 如果仍不可用,退到 `mpeg4`。 + +这解决了服务器 FFmpeg 缺少 `libx264` 时合成失败的问题。 + +当前后期策略: + +- 合成时可命名成品,默认项目名。 +- 分镜可以保留源视频声音。 +- 后期对白、字幕、BGM、音效应由前端可选。 +- 字幕不适合默认塞入长描述,当前应更偏短标题或分镜短句。 + +## 9. 当前已完成功能 + +### 基础工程 + +- NestJS 后端、Vue 前台、Vue 后台、BullMQ Worker 的 monorepo 结构。 +- Prisma + MySQL 数据层。 +- Redis 队列基础。 +- StorageService 支持本地/对象存储/临时公网 URL。 +- 全局 API 响应封装、异常处理、请求 ID。 + +### 用户与后台 + +- 用户登录、JWT、基础权限。 +- 后台 Provider 管理。 +- 后台用户、项目、素材、小说、运营配置管理。 +- Provider 日志和成本记录。 + +### 小说系统 + +- 后台小说源管理。 +- 小说简介、引文、章节管理。 +- 章节增删改查。 +- 批量粘贴导入。 +- 支持按章节分隔符切割,例如 `===== 第6章:直播里的耳光 =====`。 +- 支持卷号逻辑,当前可按 30 章一卷。 +- 前台小说列表、小说详情、目录、章节详情。 +- 阅读进度保存。 +- 书签。 +- 批注。 +- 搜索。 +- 真书翻页模式和传统按钮模式的基础入口。 + +### 提示词直出分镜 + +- 可从外部提示词创建分镜项目。 +- 支持解析 3 个镜头、不同镜头标题、不同镜头时长。 +- 支持预览镜头数量和总时长。 +- 创建后可进入真人分镜、关键帧、视频片段、合成流程。 +- 可绑定我的真人脸包,但不会直接把真人照作为角色主图。 + +### AI Provider + +- 文本、小说、图片、视频、语音、审核、向量、文件解析等 Provider 类型已建模。 +- 支持启用/停用。 +- 支持后台配置。 +- 支持模型下拉按步骤类型过滤。 +- 支持前端手动选模型。 +- 支持默认智能计划。 +- 支持 Provider 日志。 +- 支持成本估算和日限额拦截。 + +### 真人短剧 + +- 角色抽取。 +- 真人分镜准备。 +- 文本模型可参与真人分镜改写。 +- 角色锚点图生成。 +- 关键帧生成。 +- 单镜头测试生成。 +- 批量视频片段生成。 +- 多 Provider 生成候选片段。 +- 每个分镜选择候选片段。 +- 视频 Provider 预检。 +- Provider 长任务轮询和恢复。 +- 对超时但外部仍 running 的任务支持继续查询。 +- 失败片段记录。 +- 质量检查和人工审核入口。 +- FFmpeg 合成真人短剧。 + +### 素材与作品 + +- 素材表记录图片、视频、音频、字幕、最终视频等。 +- 视频片段记录 Provider、模型、成本、状态。 +- 前台作品列表已区分视频/小说/素材方向。 +- 图片和视频预览能力已具备基础形态。 + +## 10. 待优化功能 + +### Provider 能力矩阵 + +当前不同 Provider 的时长、分辨率、参考图能力不一致。需要继续把能力显式配置化: + +- 支持时长:例如 4s、5s、6s、10s。 +- 支持分辨率:例如 `720p`、`768P`、`1080P`。 +- 支持首帧。 +- 支持尾帧。 +- 支持角色参考图。 +- 是否允许真人照片。 +- 是否支持多参考图。 +- 是否支持同步声音。 + +前端应根据能力矩阵隐藏不可用选项,而不是等 Provider 报错。 + +### 外部任务与费用 + +- 已有 RenderTask 和长任务恢复,但还可以增强“正在生成”Tab。 +- 应支持取消、重新拉取、手动对账。 +- 外部官方账单 API 尚未统一接入,当前成本主要来自本地估算和 ProviderLog。 +- 需要按 Provider 建立实际账单同步策略。 + +### Prompt Builder + +- 真人视频 Prompt Builder 已成型。 +- 小说、故事圣经、角色抽取、分集、脚本、分镜等文本步骤需要统一 Prompt Builder、schema 校验和版本记录。 +- 前台应展示最终提交参数,方便测试模型质量。 + +### Character Library + +- 真人脸包和项目定妆已经拆开。 +- 需要继续强化锚点图候选管理、选中状态、历史保留。 +- 需要给用户明确说明上传脸包的照片要求。 +- 需要支持“同一脸包在不同项目生成不同定妆”的对比管理。 + +### 真人/版权安全 + +- 视频模型可能拒绝真实人物图。 +- 需要更清楚地区分本人授权、素材授权、商业可用状态。 +- 需要生成记录可追溯到使用了哪个脸包、哪个锚点、哪个 Provider、哪个 Prompt。 + +### FFmpeg / 后期 + +- 服务器字体需要稳定打包,避免中文字幕显示方框。 +- 字幕默认策略应更轻,只显示短标题或用户指定字幕。 +- 后期对白需要按分镜时长自适应,避免裁剪和重叠。 +- 源视频声音、后期配音、BGM、SFX 都应在前端合成前可选。 + +### 小说阅读器 + +- 真书翻页效果还可以继续做成点击右下角卷页,而不是按钮触发。 +- 需要继续优化分页算法、阅读主题、字体、行距、自动保存。 +- 可以增加章节购买/权限、阅读历史、多设备同步。 + +### 数据模型 + +- 当前大量关系通过 `*_id` 逻辑关联,没有全部声明 Prisma `@relation`。 +- 后续如果要生成更标准 ERD、级联删除、关联查询,可以逐步补充显式关系。 + +### 队列化 + +- 长耗时任务已有队列基础和部分恢复机制。 +- 仍建议把图片生成、视频生成、合成、质量检查统一走 RenderTask + Worker,减少 HTTP 请求长时间等待。 + +## 11. 关键文件路径 + +### 工程入口 + +- `package.json` +- `backend/src/main.ts` +- `backend/src/app.module.ts` +- `backend/prisma/schema.prisma` +- `backend/prisma/seed.ts` + +### Provider + +- `backend/src/providers/providers.service.ts` +- `backend/src/providers/providers.controller.ts` +- `backend/src/providers/provider.types.ts` +- `backend/src/providers/providers.service.spec.ts` +- `backend/prisma/migrations/20260615143000_seedance_20_providers/` +- `backend/prisma/migrations/20260615152000_volcengine_creation_providers/` +- `backend/prisma/migrations/20260615165000_openai_model_pool/` +- `backend/prisma/migrations/20260615172000_volcengine_content_generation_video/` +- `backend/prisma/migrations/20260616014000_seedance_20_cost_rules/` + +### Router + +- `backend/src/ai-router/ai-router.service.ts` +- `backend/src/ai-router/ai-router.types.ts` +- `backend/src/ai-router/ai-router.service.spec.ts` + +### Live Action + +- `backend/src/live-action/live-action.service.ts` +- `backend/src/live-action/live-action.controller.ts` +- `backend/src/live-action/live-action.dto.ts` +- `backend/src/live-action/live-action.types.ts` +- `backend/src/live-action/prompt-builder.service.ts` +- `backend/src/live-action/prompt-builder.service.spec.ts` +- `backend/src/live-action/live-action-provider-acceptance.ts` + +### Character / Image + +- `backend/src/characters/characters.service.ts` +- `backend/src/characters/characters.controller.ts` +- `backend/src/characters/character.dto.ts` +- `backend/src/characters/character.types.ts` +- `backend/src/images/images.service.ts` +- `backend/src/images/images.controller.ts` +- `backend/src/images/image.dto.ts` +- `backend/src/images/image.types.ts` +- `backend/prisma/migrations/20260615125000_character_workshop_v1/` + +### Project / Story / Script + +- `backend/src/projects/projects.service.ts` +- `backend/src/projects/projects.controller.ts` +- `backend/src/projects/project.dto.ts` +- `backend/src/story-bibles/story-bibles.service.ts` +- `backend/src/story-bibles/story-bible.dto.ts` +- `backend/src/episodes/episodes.service.ts` +- `backend/src/episodes/episode.dto.ts` +- `backend/src/scripts/scripts.service.ts` +- `backend/src/scripts/script.dto.ts` + +### Novel + +- `backend/src/novels/novels.service.ts` +- `backend/src/novels/novels.service.spec.ts` +- `backend/src/novels/novel-parser.service.ts` +- `backend/src/novels/novel-parser.service.spec.ts` +- `backend/src/novels/novel.dto.ts` +- `backend/src/novels/novel.types.ts` +- `backend/src/novels/original-novel-mock.service.ts` +- `backend/src/novels/original-novel.dto.ts` +- `backend/src/novels/original-novel.types.ts` +- `backend/prisma/migrations/20260617093000_novel_chapter_volumes/` +- `backend/prisma/migrations/20260617111500_novel_reader_tools/` + +### Media / FFmpeg + +- `backend/src/media/media.service.ts` +- `backend/src/media/media.controller.ts` +- `backend/src/media/media.dto.ts` +- `backend/src/live-action/live-action.service.ts` + +### Assets / Storage + +- `backend/src/assets/assets.service.ts` +- `backend/src/assets/assets.controller.ts` +- `backend/src/assets/storage.service.ts` +- `backend/src/assets/public-temp-assets.controller.ts` +- `backend/src/assets/asset.types.ts` + +### Queue / Worker + +- `backend/src/queues/queues.service.ts` +- `backend/src/queues/worker-tasks.controller.ts` +- `workers/src/main.ts` + +### Admin Frontend + +- `admin/src/App.vue` +- `admin/src/api/client.ts` +- `admin/src/styles.css` + +### User Frontend + +- `user-app/src/pages/index/index.vue` +- `user-app/src/api/client.ts` +- `user-app/src/styles.css` +- `user-app/src/workflow.ts` + +### Deploy / Tooling + +- `deploy/nginx.https.example.conf` +- `deploy/docker-compose.dev.yml` +- `.env.example` +- `tools/frontend-visual-audit.mjs` +- `tools/frontend-business-e2e.mjs` + +## 12. 当前 Router / Provider / Prompt / Character 的协作关系 + +```mermaid +flowchart TB + UI[前端步骤模型选择\n智能计划 / 手动模型] --> LiveAPI[LiveActionController] + LiveAPI --> LiveService[LiveActionService] + + LiveService --> CharacterCtx[角色上下文\nCharacter / ActorProfile / FacePack / Anchor] + LiveService --> Router[AiRouterService] + Router --> ProviderConfig[ProviderConfig\n启用状态 / 成本 / 能力] + Router --> Decision[RouterDecision\nprovider / fallback / cost / tier] + + CharacterCtx --> PromptBuilder[PromptBuilderService] + Decision --> PromptBuilder + PromptBuilder --> FinalPrompt[最终视频 Prompt\nPrompt Components / Negative Prompt] + + FinalPrompt --> ProviderService[ProvidersService] + ProviderService --> ExternalAI[外部 AI 平台] + ExternalAI --> Asset[Asset / VideoClip / RenderTask] +``` + +关键原则: + +- 前端每个 AI 步骤应只展示对应类型模型:文本、图片、视频、语音。 +- 用户不选模型时走智能计划。 +- 用户手动选模型时必须真实生效;如果不可用应直接报错。 +- 同一分镜可以用多个视频模型生成多个候选。 +- 合成前必须选择每个分镜最终使用哪个候选片段。 +- 真人脸包只锁脸,项目锚点图才是视频角色主参考。 +- Provider 报 running 时,本地不应直接判失败,应记录外部任务号并继续轮询恢复。 -1. 真实 Provider 准入矩阵。 -2. 队列化/失败恢复。 -3. Prompt Library 与拉片模式库联动。 -4. 角色库运营化。 -5. 真实成本/ROI 回流。 diff --git a/NOVEL_IP_SYSTEM_DESIGN.md b/NOVEL_IP_SYSTEM_DESIGN.md new file mode 100644 index 0000000..7d5dcfc --- /dev/null +++ b/NOVEL_IP_SYSTEM_DESIGN.md @@ -0,0 +1,1288 @@ +# NOVEL_IP_SYSTEM_DESIGN + +状态:设计落地基线 +创建时间:2026-06-17 +适用范围:小说原创生产、小说阅读、听书派生、AI 短剧派生、IP 资产管理。 +核心目标:高质量、低人工干预、长上下文不乱,并且天然适配听书和短剧。 + +## 1. 最高原则 + +这个系统不是“AI 帮我写一章小说”,而是: + +```text +AI 主编室 ++ AI 作家组 ++ AI 审稿组 ++ AI 档案管理员 ++ AI 改编编剧室 ++ AI 听书制作室 ++ AI 短剧制作室 +``` + +所有设计都围绕四个优先级: + +```text +小说质量 > 上下文稳定 > 自动化效率 > 听书/短剧派生 +``` + +硬性原则: + +1. 小说正文是母体,听书和短剧是派生作品。 +2. 听书和短剧必须基于小说版本快照,不直接依赖实时正文。 +3. AI 可以自动执行,但不能擅自修改 IP 圣经、人物命运、重大反转和禁止规则。 +4. 每章必须有章节卡、质检报告、记忆更新。 +5. 每次重写、润色、修复都必须留版本。 +6. 自动化必须有停机规则,不能连续生成低质量内容。 +7. 角色、场景、道具、声音、视觉风格要资产化,不能每章重新发明。 +8. 短剧系统不并入小说系统,听书系统也不并入小说系统;它们通过快照和派生任务关联。 + +## 2. 总体架构 + +```mermaid +flowchart TB + Brief[创作 Brief] --> Plan[NovelGenerationPlan\n生成计划] + Plan --> IPBible[IP 圣经\n故事/世界/人物/改编规则] + IPBible --> Assets[IP 资产库\n角色/场景/道具/声音/视频风格] + IPBible --> Outline[全书结构 / 分卷结构 / 章节规划] + Assets --> ChapterLoop[章节自动生产循环] + Outline --> ChapterLoop + + ChapterLoop --> Card[章节卡] + Card --> Draft[正文初稿] + Draft --> Polish[润色稿] + Polish --> Quality[质量闸门] + Quality -->|通过| Final[终稿入库 NovelChapter] + Quality -->|局部问题| Repair[自动修复] + Quality -->|严重问题| Rewrite[整章重写] + Repair --> Quality + Rewrite --> Quality + Quality -->|多次失败| Review[人工审核] + + Final --> Memory[上下文记忆更新] + Final --> Package[章节改编包\n听书/短剧可用结构] + Memory --> Next[下一章] + Package --> Snapshot[NovelVersionSnapshot] + + Snapshot --> Audio[AudioBookProject\n听书派生] + Snapshot --> Drama[Drama Adaptation\n短剧派生] + Drama --> ExistingLive[现有 LiveActionModule\n关键帧/视频/合成] +``` + +## 3. 小说类型策略 + +不同体量不能用同一套上下文策略。 + +| 类型 | 建议规模 | 生成策略 | 上下文策略 | 人工闸门 | +| --- | ---: | --- | --- | --- | +| 短篇 | 1-5 章 / 5千-3万字 | 先完整设计结构,再逐章生成 | 可带较完整全局上下文 | 大纲确认 + 完结确认 | +| 中篇 | 6-30 章 / 3万-15万字 | 按 5-10 章批次生成 | IP 圣经 + 最近章节 + 关键记忆 | 每 10 章人工总审 | +| 长篇 | 30-300+ 章 / 15万字以上 | 按卷、按 10 章校准 | IP 圣经 + 卷纲 + 检索记忆 + 伏笔库 | 每卷/每 10 章检查 | + +后台需要提供 `novel_scale`: + +```text +short +medium +long +``` + +对应自动化等级: + +```text +L1: 每章人工确认 +L2: 自动 5-10 章,批次后人工总审 +L3: 按卷自动,触发风险才暂停 +``` + +第一阶段推荐启用 L1/L2,终局必须支持 L3。 + +## 4. 现有系统复用边界 + +当前项目已有很多基础,不要重复造。 + +### 继续复用 + +| 现有模型/模块 | 继续承担的职责 | +| --- | --- | +| `Project` | 所有作品的顶层容器,包括小说、短剧、派生项目 | +| `NovelSource` | 小说源头,导入文本或原创小说主体 | +| `NovelChapter` | 已发布或确认的章节终稿 | +| `StoryBible` | 故事核心、主线、卖点、禁忌规则,可作为 IP 圣经文本层 | +| `WorldBible` | 世界观、规则、时代、视觉规则 | +| `Character` | 项目角色档案、视觉、声音、表演风格 | +| `GlobalCharacter` | 我的长期真人脸包/全局角色 | +| `CharacterImage` | 角色定妆、锚点图、脸包引用 | +| `CharacterState` | 角色不同状态、服装、情绪、状态变体 | +| `ActorProfile` | 真人短剧角色参考配置 | +| `Episode` | 短剧分集 | +| `EpisodeScript` | 短剧剧本 | +| `StoryboardShot` | 真人短剧分镜和视频提示词承载 | +| `Asset` | 图片、视频、音频、字幕、成品文件 | +| `RenderTask` | 图片/视频/合成等长任务 | +| `ProviderConfig` | AI Provider 配置 | +| `ProviderLog` | AI 调用日志和成本记录 | +| `LiveActionModule` | 真人短剧生产链路,不并入小说系统 | +| `MediaModule` | 音频/字幕/视频合成能力 | + +### 不建议重复新增 + +不要新增: + +```text +novel_projects +novel_characters +novel_video_pipeline +``` + +原因: + +- `Project` 已经能当项目容器。 +- `Character` 已经有足够多角色字段,应增强而不是重建。 +- 视频流水线已经在 `LiveActionModule`,小说只负责适配和派生。 + +## 5. 需要新增的核心能力 + +第一版可以少启用 Agent,但这些地基必须先设计好: + +```text +Agent 注册表 +Prompt 版本管理 +AgentRun 调用记录 +Workflow 状态机 +Novel Context Builder +Quality Gate +Memory / Foreshadow +Chapter Version +Novel Snapshot +Derivative Job +IP Asset +Human Review Gate +``` + +## 6. 推荐新增数据模型 + +字段命名建议跟当前 Prisma 风格保持一致:`snake_case`、`BigInt` 自增、MySQL、JSON 字段承载可扩展结构。 + +### 6.1 NovelGenerationPlan + +记录原创小说生产计划和自动化配置。 + +```text +NovelGenerationPlan + id + project_id + novel_source_id + user_id + novel_scale short / medium / long + target_words + target_chapters + genre + style_code + brief_json + ip_bible_json + volume_plan_json + pipeline_config_json + quality_threshold_json + automation_level L1 / L2 / L3 + status draft / planning / writing / paused / completed / failed + current_chapter_no + created_at + updated_at +``` + +说明: + +- `Project` 仍然是父级项目。 +- `NovelGenerationPlan` 只是原创生产计划,不替代 `Project`。 +- `pipeline_config_json` 记录是否允许自动听书、自动短剧、批量章数、人工闸门等。 + +### 6.2 NovelChapterVersion + +每章的章节卡、初稿、润色稿、修复稿、终稿都要留版本。 + +```text +NovelChapterVersion + id + project_id + novel_source_id + novel_chapter_id + chapter_no + version_no + version_type chapter_card / draft / polished / repaired / final + title + content_text + content_json + source_agent + provider_code + model_name + quality_score + status draft / active / archived / rejected + created_at +``` + +说明: + +- `NovelChapter` 只保存当前确认终稿。 +- `NovelChapterVersion` 保存生产过程。 +- 以后用户觉得某版更好,可以回滚。 + +### 6.3 NovelContextMemory + +小说专用上下文记忆,和现有 `PlotMemory` 不冲突。 + +```text +NovelContextMemory + id + project_id + novel_source_id + chapter_no + memory_type + memory_text + memory_json + importance_level + status + created_at +``` + +`memory_type`: + +```text +bible_summary +chapter_summary +character_state +relationship_state +world_update +timeline_update +location_update +style_rules +adaptation_notes +forbidden_to_forget +``` + +说明: + +- `PlotMemory` 现在更偏分集/短剧连续性。 +- 小说长篇需要 `novel_source_id`、`chapter_no`、结构化 JSON,所以单独建表更稳。 + +### 6.4 NovelForeshadow + +伏笔管理是长篇不乱的核心。 + +```text +NovelForeshadow + id + project_id + novel_source_id + code + title + first_chapter_no + expected_reveal_chapter_no + actual_reveal_chapter_no + surface_text + hidden_truth + status planned / introduced / developing / revealed / abandoned + related_characters_json + related_assets_json + created_at + updated_at +``` + +硬规则: + +- `project_id + code` 唯一。 +- 如果当前章节超过预计回收章数太多,后台要提醒。 + +### 6.5 NovelQualityReport + +每次质检都入库。 + +```text +NovelQualityReport + id + project_id + novel_source_id + novel_chapter_id + chapter_no + report_type draft / polished / final / batch_review + total_score + score_json + problems_json + suggestions_json + pass_status + rewrite_required + source_agent + provider_code + model_name + created_at +``` + +评分维度: + +```text +plot_progress +character_consistency +continuity +emotion_depth +style_quality +foreshadow_management +scene_visualization +audio_adaptation +video_adaptation +``` + +### 6.6 NovelVersionSnapshot + +派生听书/短剧必须基于快照。 + +```text +NovelVersionSnapshot + id + project_id + novel_source_id + version_no + title + snapshot_scope full_novel / chapter_range / single_chapter + chapter_start + chapter_end + source_hash + snapshot_text + snapshot_json + created_for audiobook / short_drama / export / manual + created_by_user_id + created_at +``` + +规则: + +- 启动听书或短剧前必须创建快照。 +- 小说后续修改不影响已创建快照。 +- 重新生成派生作品时,用户选择使用旧快照还是最新快照。 + +### 6.7 NovelDerivativeJob + +统一管理小说派生任务。 + +```text +NovelDerivativeJob + id + project_id + novel_source_id + snapshot_id + user_id + derivative_type audiobook / short_drama + target_project_id + target_ref_id + chapter_start + chapter_end + config_json + status pending / running / paused / completed / failed + progress_json + error_message + created_at + updated_at +``` + +说明: + +- 它管理“从小说派生作品”的过程。 +- `RenderTask` 继续管理图片、视频、合成等底层耗时任务。 + +### 6.8 ProjectIpAsset + +项目级 IP 资产,承载场景、道具、声音、视觉风格。 + +```text +ProjectIpAsset + id + project_id + asset_type scene / prop / costume / music_style / video_style / voice_profile + asset_key + display_name + asset_json + prompt_base + negative_prompt + reference_asset_id + status + created_at + updated_at +``` + +说明: + +- 角色不用放这里,继续用 `Character`。 +- 场景、道具、服装、声音风格、视频风格放这里。 +- 短剧生成时引用 `asset_key`,避免每章重新发明场景。 + +### 6.9 AgentPrompt + +Agent Prompt 版本管理。 + +```text +AgentPrompt + id + agent_name + version + provider_type + default_provider_code + system_prompt + user_prompt_template + output_schema_json + temperature + max_output_tokens + is_active + created_at +``` + +说明: + +- Prompt 不应写死在代码里。 +- 后台可以调整 Prompt 版本。 +- 设计类 Agent 必须有 `output_schema_json`。 + +### 6.10 AgentRun + +每次 Agent 调用记录。 + +```text +AgentRun + id + project_id + novel_source_id + chapter_no + agent_name + prompt_version + provider_code + model_name + input_json + output_json + output_text + status + quality_score + error_code + error_message + started_at + finished_at + created_at +``` + +说明: + +- 用来追踪 AI 为什么这样写。 +- 用来成本统计、重试、回放、调参。 +- 和 `ProviderLog` 的区别:`ProviderLog` 记录底层 Provider 调用,`AgentRun` 记录业务语义级 Agent 调用。 + +### 6.11 AudioBookProject + +听书独立项目。 + +```text +AudioBookProject + id + project_id + novel_source_id + snapshot_id + user_id + title + status + voice_config_json + audio_style_json + progress_json + created_at + updated_at +``` + +可以第二阶段或第三阶段再实现,但表关系第一版要预留。 + +### 6.12 AudioBookChapter + +听书章节。 + +```text +AudioBookChapter + id + audiobook_project_id + novel_chapter_id + chapter_no + audio_script_text + segment_json + subtitle_json + estimated_duration_sec + audio_asset_id + status + error_message + created_at + updated_at +``` + +## 7. Agent 终局设计 + +第一版不一定全部启用,但系统必须能容纳完整 Agent。 + +### 7.1 小说创作线 + +| Agent | 职责 | 第一阶段 | +| --- | --- | --- | +| `IPBibleAgent` | 生成 IP 圣经、主线、人设、风格、禁忌、改编规则 | 启用 | +| `VolumeOutlineAgent` | 生成全书/分卷/批次大纲 | 部分启用 | +| `ChapterCardAgent` | 生成章节卡,控制当章目标和禁止事项 | 启用 | +| `NovelWriterAgent` | 生成正文初稿 | 启用 | +| `NovelPolishAgent` | 润色文风和节奏 | 启用 | +| `ContinuityCheckAgent` | 检查设定、人设、时间线冲突 | 启用 | +| `QualityCheckAgent` | 评分、给出问题和修复策略 | 启用 | +| `RepairAgent` | 局部修复或整章重写 | 启用 | +| `MemoryUpdateAgent` | 更新章节摘要、人物状态、时间线 | 启用 | +| `ForeshadowAgent` | 新增/推进/回收伏笔 | 第二阶段 | + +### 7.2 IP 资产线 + +| Agent | 职责 | 第一阶段 | +| --- | --- | --- | +| `CharacterAssetAgent` | 生成人物文字/视觉/声音基础档案 | 部分复用角色抽取 | +| `SceneAssetAgent` | 生成可复用场景资产 | 预留 | +| `PropAssetAgent` | 生成道具资产 | 预留 | +| `VoiceProfileAgent` | 生成角色声音档案 | 预留 | +| `AdaptationPackageAgent` | 抽取可听/可拍/可视频化结构 | 启用轻量版 | + +### 7.3 听书线 + +| Agent | 职责 | 启用阶段 | +| --- | --- | --- | +| `AudioScriptAgent` | 小说正文转听书稿 | 第三阶段 | +| `VoiceSegmentAgent` | 旁白/对白/情绪/停顿/TTS 分段 | 第三阶段 | +| `AudioQualityAgent` | 检查听书稿可听性、角色声线一致性 | 第三阶段 | + +### 7.4 短剧线 + +| Agent | 职责 | 启用阶段 | +| --- | --- | --- | +| `DramaScriptAgent` | 小说章节转短剧剧本 | 第四阶段 | +| `ShotListAgent` | 短剧剧本转分镜 | 第四阶段 | +| `VideoPromptAgent` | 分镜转视频提示词 | 第四阶段 | +| `VideoPromptQualityAgent` | 检查提示词是否可生成、是否超复杂 | 第四阶段 | +| `PublishPackageAgent` | 汇总小说/听书/短剧/封面/简介/标签 | 第五阶段 | + +## 8. 第一版启用 Agent + +第一版建议启用 8 个核心 Agent: + +```text +IPBibleAgent +ChapterCardAgent +NovelWriterAgent +NovelPolishAgent +ContinuityCheckAgent +QualityCheckAgent +RepairAgent +MemoryUpdateAgent +``` + +同时预留: + +```text +AdaptationPackageAgent +DramaScriptAgent +AudioScriptAgent +``` + +第一版目标不是做全套听书短剧,而是让小说质量闭环稳定。 + +## 9. Context Builder 设计 + +`NovelContextBuilderService` 是核心,不是可选项。 + +每次写第 N 章,不能把全文塞给模型,而是自动拼: + +```text +IP 圣经摘要 +当前卷纲 +目标章节卡 +最近 3-5 章摘要 +本章出场人物档案 +本章出场人物当前状态 +相关伏笔 +当前时间线 +当前空间位置 +禁止改动项 +文风规则 +改编规则 +``` + +短篇、中篇、长篇策略: + +```text +短篇:可带完整大纲和全部已写章节摘要 +中篇:带当前卷/批次大纲 + 最近 5 章摘要 +长篇:带 IP 圣经摘要 + 当前卷纲 + 检索相关记忆 + 最近 5 章摘要 +``` + +早期检索策略: + +```text +MySQL 条件检索 + project_id + chapter_no 范围 + memory_type + character names + foreshadow status +``` + +后期增强: + +```text +Embedding 检索 + NovelContextMemory + NovelChapter.summary + CharacterMemory + NovelForeshadow +``` + +## 10. Quality Gate 设计 + +每章必须经过质量闸门。 + +评分维度: + +| 维度 | 权重 | +| --- | ---: | +| 剧情推进 | 15 | +| 人物一致性 | 15 | +| 上下文连续性 | 15 | +| 文风质量 | 12 | +| 情绪张力 | 12 | +| 伏笔管理 | 10 | +| 场景画面感 | 8 | +| 听书适配度 | 6 | +| 短剧适配度 | 7 | + +默认规则: + +```text +>= 88:自动通过 +80-87:自动局部修复 +< 80:整章重写 +同一章最多重写 3 次 +3 次仍不通过:need_human_review +连续 3 章低于 85:暂停批量自动生成 +出现核心设定冲突:暂停 +人物死亡、重大秘密揭露、主线反转:人工审核 +``` + +质量报告必须写入 `NovelQualityReport`。 + +## 11. 章节内容包 + +每章最终不只是正文,还要有改编结构。 + +```json +{ + "chapter_no": 1, + "title": "查无此人", + "novel_text": "", + "chapter_summary": "", + "memory_update": {}, + "adaptation_package": { + "audio_candidates": [], + "drama_scenes": [], + "visual_hooks": [], + "dialogue_candidates": [], + "narration_candidates": [], + "character_refs": [], + "scene_refs": [], + "prop_refs": [], + "ending_hook": "" + }, + "quality": { + "novel_score": 88, + "audio_adaptation_score": 82, + "video_adaptation_score": 86 + } +} +``` + +这个结构建议存在: + +- `NovelChapterVersion.content_json` +- 或 `NovelContextMemory` 的 `adaptation_notes` +- 后续可抽成单独 `ChapterAdaptationPackage` + +第一版先不新增 `ChapterAdaptationPackage` 表,避免过重。 + +## 12. 小说到听书 + +听书线必须独立。 + +```mermaid +flowchart LR + Novel[NovelSource / NovelChapter] --> Snapshot[NovelVersionSnapshot] + Snapshot --> AudioProject[AudioBookProject] + AudioProject --> AudioScript[AudioScriptAgent] + AudioScript --> VoiceSegment[VoiceSegmentAgent] + VoiceSegment --> VoiceProvider[VoiceProvider] + VoiceProvider --> AudioAsset[Asset audio] + AudioAsset --> AudioChapter[AudioBookChapter] +``` + +规则: + +1. 小说正文不能直接 TTS。 +2. 必须先转听书稿。 +3. 听书稿要拆旁白、对白、情绪、停顿、BGM、音效建议。 +4. 听书失败不影响小说正文。 +5. 小说修改后不自动覆盖已生成听书,除非用户选择新快照重做。 + +## 13. 小说到短剧 + +短剧线通过适配层进入现有 LiveAction。 + +```mermaid +flowchart LR + Novel[NovelVersionSnapshot] --> Adapter[NovelToDramaAdapter] + Adapter --> Project[生成/关联短剧 Project] + Project --> Story[StoryBible / WorldBible / Character] + Story --> Episode[Episode / EpisodeScript] + Episode --> Shots[StoryboardShot] + Shots --> Live[LiveActionModule] + Live --> Video[关键帧 / 视频片段 / 候选选择 / FFmpeg 合成] +``` + +`NovelToDramaAdapter` 负责: + +```text +读取快照 +选择章节范围 +提取核心冲突 +提取可视化场景 +提取关键对白 +提取角色/场景/道具引用 +生成短剧 Project +生成 Episode +生成 EpisodeScript +生成 StoryboardShot +交给 LiveActionModule +``` + +规则: + +- 小说章节是原料,不是剧本。 +- 短剧不要直接塞大量小说旁白。 +- 每个分镜都要引用已有角色/场景/道具资产。 +- 合成前继续使用现有候选片段选择机制。 + +## 14. Provider 和模型策略 + +不要所有 Agent 都用最贵模型。 + +| 类型 | 适用 Agent | 模型策略 | +| --- | --- | --- | +| 高模型 | IP 圣经、人物设定、正文写作、最终润色 | 质量优先 | +| 中模型 | 章节卡、卷纲、记忆更新、伏笔更新 | 稳定结构 | +| 低模型 | 标签、摘要、格式转换、简单检查 | 成本优先 | +| 检查模型 | 连续性、质量评分、Prompt 质检 | 低温度、结构化输出 | + +所有 Agent 都应支持: + +```text +智能计划 +手动指定模型 +失败 fallback +ProviderLog +AgentRun +成本估算 +``` + +设计类 Agent 必须使用 JSON Schema 或强结构化解析。 + +正文类 Agent 可以输出纯文本,但调用记录仍要保存。 + +## 15. 后台页面设计 + +后台小说详情建议分为这些 Tab: + +```text +概览 +正文 +IP 圣经 +角色资产 +场景/道具 +章节生产 +记忆/伏笔 +质检报告 +听书 +短剧 +Agent 配置 +任务队列 +``` + +### 概览 + +显示: + +- 小说类型:短篇/中篇/长篇。 +- 自动化等级:L1/L2/L3。 +- 当前章节。 +- 最近质量评分。 +- 伏笔风险。 +- 派生作品数量。 +- 当前任务状态。 + +### IP 圣经 + +显示: + +- 一句话核心。 +- 主题。 +- 世界观。 +- 主线。 +- 人物命运。 +- 禁止改动项。 +- 改编规则。 +- 版本记录。 + +### 章节生产 + +显示: + +- 章节卡。 +- 初稿。 +- 润色稿。 +- 修复稿。 +- 终稿。 +- 质检报告。 +- AgentRun 记录。 + +操作: + +- 生成下一章。 +- 连写 5 章。 +- 连写 10 章。 +- 暂停。 +- 重写本章。 +- 使用某版本为终稿。 +- 人工通过。 + +### 记忆/伏笔 + +显示: + +- 最近章节摘要。 +- 人物状态变化。 +- 关系变化。 +- 时间线。 +- 位置变化。 +- 伏笔状态。 +- 逾期伏笔提醒。 + +### 听书 + +显示: + +- 是否启用听书。 +- 自动/手动生成配置。 +- 快照选择。 +- 听书项目列表。 +- 每章音频状态。 +- 试听。 +- 重新生成。 + +### 短剧 + +显示: + +- 是否启用短剧。 +- 自动/手动生成配置。 +- 快照选择。 +- 派生短剧项目。 +- 跳转真人短剧工作台。 +- 成片预览。 + +## 16. 前台页面设计 + +前台不暴露复杂生产逻辑,只显示用户要看的内容: + +```text +小说 +听书 +短剧 +素材 +成品 +``` + +小说阅读器继续优化: + +- 真书翻页模式。 +- 传统上下章模式。 +- 阅读进度。 +- 书签。 +- 批注。 +- 搜索。 + +听书: + +- 按小说/章节展示。 +- 支持播放。 +- 支持断点续播。 + +短剧: + +- 按小说/项目/集展示。 +- 支持播放。 +- 支持查看来源章节和快照版本。 + +## 17. API 设计草案 + +### 生产计划 + +```text +POST /api/novels/:novelSourceId/generation-plans +GET /api/novels/:novelSourceId/generation-plans/:planId +PATCH /api/novels/:novelSourceId/generation-plans/:planId +POST /api/novels/:novelSourceId/generation-plans/:planId/start +POST /api/novels/:novelSourceId/generation-plans/:planId/pause +``` + +### Agent 执行 + +```text +POST /api/novels/:novelSourceId/agents/ip-bible/generate +POST /api/novels/:novelSourceId/chapters/:chapterNo/card/generate +POST /api/novels/:novelSourceId/chapters/:chapterNo/draft/generate +POST /api/novels/:novelSourceId/chapters/:chapterNo/polish +POST /api/novels/:novelSourceId/chapters/:chapterNo/quality-check +POST /api/novels/:novelSourceId/chapters/:chapterNo/repair +POST /api/novels/:novelSourceId/chapters/:chapterNo/memory-update +``` + +### 章节版本 + +```text +GET /api/novels/:novelSourceId/chapters/:chapterNo/versions +POST /api/novels/:novelSourceId/chapters/:chapterNo/versions/:versionId/activate +``` + +### 记忆与伏笔 + +```text +GET /api/novels/:novelSourceId/memories +GET /api/novels/:novelSourceId/foreshadows +POST /api/novels/:novelSourceId/foreshadows +PATCH /api/novels/:novelSourceId/foreshadows/:id +``` + +### 快照与派生 + +```text +POST /api/novels/:novelSourceId/snapshots +GET /api/novels/:novelSourceId/snapshots +POST /api/novels/:novelSourceId/derivatives/audiobook +POST /api/novels/:novelSourceId/derivatives/short-drama +GET /api/novels/:novelSourceId/derivative-jobs +``` + +## 18. BullMQ 任务设计 + +建议新增任务类型: + +```text +novel.ip_bible.generate +novel.volume_outline.generate +novel.chapter_card.generate +novel.chapter_draft.generate +novel.chapter_polish.generate +novel.chapter_quality_check +novel.chapter_repair +novel.memory_update +novel.batch_generate +novel.snapshot.create +novel.audiobook.create +novel.short_drama.adapt +``` + +每个任务都要: + +- 写 `AgentRun`。 +- 写 `ProviderLog`。 +- 支持重试。 +- 支持暂停。 +- 支持失败原因。 +- 支持人工接管。 + +## 19. 工作流状态机 + +### NovelGenerationPlan.status + +```text +draft +planning +ready +writing +paused +need_human_review +completed +failed +``` + +### NovelChapterVersion.status + +```text +draft +active +archived +rejected +``` + +### NovelDerivativeJob.status + +```text +pending +running +paused +completed +failed +cancelled +``` + +### AudioBookProject.status + +```text +draft +generating_script +generating_voice +mixing +completed +failed +paused +``` + +### 短剧 Project.status 补充建议 + +```text +drama_adapting +storyboard_ready +live_action_ready +video_generating +clip_selecting +rendering +completed +failed +``` + +## 20. 分阶段落地计划 + +### Phase 0:设计冻结 + +目标: + +- 固定本文档。 +- 明确复用现有表和新增表。 +- 明确第一版启用 Agent 和终局 Agent。 + +交付: + +- `NOVEL_IP_SYSTEM_DESIGN.md` +- 后续开发任务清单。 + +### Phase 1:小说 IP 生产地基 + +目标: + +- 建表。 +- 建 Agent Prompt/Run。 +- 建 NovelGenerationPlan。 +- 建 ChapterVersion。 +- 建 ContextMemory。 +- 建 QualityReport。 +- 建 Snapshot。 +- 建 DerivativeJob。 +- 建基础后台页面。 + +不要求一次生成完美小说,但要求所有过程可追踪。 + +### Phase 2:高质量单章闭环 + +目标: + +```text +Brief +→ IP 圣经 +→ 章节卡 +→ 正文初稿 +→ 润色 +→ 连续性检查 +→ 质量评分 +→ 修复/重写 +→ 终稿入库 +→ 记忆更新 +``` + +验收标准: + +- 每一步有 AgentRun。 +- 每一章有质量分。 +- 低分会自动修复。 +- 修复失败会暂停。 +- 上下文不会靠手工粘贴。 + +### Phase 3:批量自动生成 + +目标: + +- 支持 5 章/10 章批量生成。 +- 支持短篇、中篇、长篇策略。 +- 支持每 10 章自动体检。 +- 支持伏笔逾期提醒。 +- 支持人工审核闸门。 + +验收标准: + +- 连续生成 10 章不明显断上下文。 +- 人物状态能延续。 +- 伏笔能新增、推进、回收。 +- 低分章节不会直接发布。 + +### Phase 4:听书派生 + +目标: + +```text +NovelVersionSnapshot +→ AudioBookProject +→ AudioScriptAgent +→ VoiceSegmentAgent +→ VoiceProvider +→ Asset 音频 +``` + +验收标准: + +- 听书基于快照。 +- 小说修改不影响已生成听书。 +- 听书稿不是直接 TTS 正文。 +- 每章音频可播放。 + +### Phase 5:短剧派生 + +目标: + +```text +NovelVersionSnapshot +→ NovelToDramaAdapter +→ Project / Episode / Script / StoryboardShot +→ LiveActionModule +``` + +验收标准: + +- 可选择小说章节范围生成短剧项目。 +- 自动生成短剧剧本和分镜。 +- 分镜引用角色/场景/道具资产。 +- 后续进入现有真人短剧流程。 + +### Phase 6:IP 资产强化 + +目标: + +- 角色视觉资产稳定。 +- 角色声音资产稳定。 +- 场景/道具/服装/音乐/视频风格资产可复用。 +- 视频 Prompt 不再每章乱写角色和场景。 + +### Phase 7:全自动与运营化 + +目标: + +- L3 按卷自动生成。 +- 自动生成封面提示词、简介、标签、预告片脚本。 +- 支持平台版本:小说平台、听书平台、短剧平台。 +- 支持成本、质量、失败率统计。 + +## 21. 第一轮开发任务拆分 + +建议按这个顺序开发,不容易翻车: + +1. Prisma 新增基础表。 +2. 新增 `AgentPrompt` 初始化数据。 +3. 新增 `AgentRunService`,封装业务级 Agent 调用。 +4. 新增 `NovelPromptBuilderService`。 +5. 新增 `NovelContextBuilderService`。 +6. 新增 `NovelQualityGateService`。 +7. 新增 `NovelGenerationWorkflowService`。 +8. 后台新增小说生产 Tab。 +9. 跑通 IP 圣经生成。 +10. 跑通单章生成闭环。 +11. 加入 Snapshot。 +12. 加入派生任务空壳。 + +第一轮不要急着接听书和短剧视频,先把小说质量闭环跑稳。 + +## 22. 验收清单 + +### 小说质量 + +- 能根据 Brief 生成 IP 圣经。 +- 能生成章节卡。 +- 能根据上下文生成正文。 +- 能自动质检。 +- 能自动修复。 +- 能保存版本。 +- 能更新记忆。 +- 能管理伏笔。 + +### 自动化 + +- 人工不需要复制上下文。 +- 人工不需要手工整理最近章节摘要。 +- 人工只在质量闸门或重大节点介入。 + +### 长上下文稳定 + +- 第 N 章能读取最近 3-5 章摘要。 +- 能读取出场人物当前状态。 +- 能读取相关伏笔。 +- 能读取禁止改动项。 +- 能识别设定冲突。 + +### 听书/短剧适配 + +- 每章有改编包。 +- 每次派生有快照。 +- 派生任务失败不污染小说正文。 +- 短剧最终进入现有 LiveActionModule。 + +## 23. 不做的事 + +第一阶段不做: + +- 一键生成 100 章。 +- 自动生成完整听书成品。 +- 自动生成完整短剧成片。 +- 替换现有 LiveActionModule。 +- 重建角色系统。 +- 重建视频任务系统。 + +第一阶段要做的是把“小说质量闭环 + 可追踪地基”打稳。 + +## 24. 最终判断 + +当前项目已经有视频流水线、角色库、Provider、素材、FFmpeg、小说导入和阅读器。现在正确方向不是重做,而是补上: + +```text +Novel IP 生产中台 +``` + +它负责: + +- 写出高质量小说。 +- 管住长篇上下文。 +- 管住人物和伏笔。 +- 管住版本和快照。 +- 把小说变成可听、可拍、可生成视频的内容包。 + +最终系统形态: + +```text +小说能独立成为作品 +听书能从小说快照派生 +短剧能从小说快照派生 +三个系统互相关联,但互不污染 +``` + diff --git a/PROJECT_STATUS_V1.md b/PROJECT_STATUS_V1.md new file mode 100644 index 0000000..514b968 --- /dev/null +++ b/PROJECT_STATUS_V1.md @@ -0,0 +1,879 @@ +# AI 内容生产平台项目现状 V1 + +> 文档类型:项目状态回灌 / 技术审计快照 +> 审计时间:2026-07-15(Europe/Berlin) +> 审计对象:`/www/wwwroot/ai` 当前工作区、当前 MySQL 数据库、当前 systemd 运行服务 +> 文档用途:作为 Work 的“代码现实”基线,不替代产品设计文档,也不替代 Git 版本记录。 + +## 0. 审计口径 + +本文件按以下优先级判断项目真实状态: + +1. 当前工作区源码与配置结构。 +2. Prisma schema、迁移记录和数据库只读统计。 +3. 当前 systemd、Nginx、MySQL、Redis 运行状态。 +4. 构建与自动测试结果。 +5. `README.md`、`CURRENT_ARCHITECTURE.md`、`CODEX_PROGRESS.md` 与 `docs/system_a`、`docs/system_b` 等历史文档。 + +状态标记: + +| 标记 | 含义 | +| --- | --- | +| 已实现且验证 | 代码存在,当前可构建,并有运行数据、测试或实际调用记录佐证 | +| 已实现但验证不足 | 主体代码已存在,但自动测试、运行配置或完整 E2E 尚未达到发布基线 | +| 部分接入 | 只有部分入口、部分 Provider 或部分流程可用 | +| 仅设计/预留 | 有文档、数据结构或界面占位,但未形成完整可用链路 | + +注意:本文中的 V1 是“状态文档版本”,不是 npm 包版本。当前各 workspace 的代码版本仍为 `0.1.0`,不应仅凭功能变多就直接对外宣布产品 V5.0。 + +## 1. 执行摘要 + +### 1.1 当前项目真实定位 + +当前系统已经不再只是“AI 小说系统”或“AI 漫剧系统”,实际代码覆盖: + +- 小说导入、阅读、原创生成、章节质量循环与版本管理。 +- 故事圣经、世界观、角色、剧情记忆与 IP 资产管理。 +- 分集、剧本、分镜、关键帧、图生视频和候选片段选择。 +- 真人/仿真人短剧生产、原生音频视频、字幕、BGM、SFX、转场与 FFmpeg 合成。 +- AI Provider 目录、模型偏好、成本记录、路由、实验台与回退。 +- 队列、任务重试、人工介入、额度、审核、运营后台和审计日志。 +- 面向生产的独立创作工具,当前已开放“人物三视图”。 + +因此,面向 Work 的项目名称建议使用: + +```text +AI 内容生产平台 +``` + +### 1.2 当前健康度 + +| 检查项 | 当前结论 | +| --- | --- | +| 后端 API | `ai-backend.service` 正在运行,健康检查返回 `ok` | +| 队列 Worker | `ai-workers.service` 正在运行,消费 14 个 BullMQ 队列 | +| 数据库 | MySQL 正在运行,33 个 Prisma 迁移全部已部署 | +| Redis | 正在运行,Worker 与 API 均配置为使用 Redis | +| 前台/后台 | Vue 3 + Vite 生产构建通过,Nginx 直接托管静态产物 | +| 全仓构建 | 通过:backend、admin、user-app、workers 均成功构建 | +| 自动测试 | 未通过:后端 273 条测试中 238 通过、35 失败;Worker 1 条通过;前台和后台无测试文件 | +| Git 基线 | 高风险:`main` 仅 1 个提交,当前有 85 个修改项、47 个未跟踪项 | +| 运行版本一致性 | 需处理:审计构建产物晚于 backend/worker 进程启动时间,当前进程尚未加载最新构建 | +| 存储 | 当前为本机私有存储;MinIO 代码已实现但当前未完整配置 | +| AI 默认模式 | 全局 `AI_PROVIDER_MODE=mock`,但数据库中有大量真实 Provider 可被显式选择 | + +### 1.3 结论 + +当前项目已经是一个有真实数据、真实调用和真实视频资产的内部生产平台,不是原型空壳;但它还不是可安全标记为“稳定生产版”的发布基线。 + +当前最重要的工作顺序应为: + +1. 固化 Git 与数据库备份基线。 +2. 修复测试和状态枚举漂移。 +3. 对齐当前构建与运行进程。 +4. 补齐队列任务执行覆盖。 +5. 再继续扩展新功能。 + +## 2. 当前项目实际架构 + +### 2.1 仓库形态 + +项目为 npm workspaces 单仓库: + +```text +ai/ +├── backend/ NestJS API、Prisma、业务编排、Provider、FFmpeg 调用 +├── workers/ BullMQ Worker,消费队列后委托 backend 执行业务 +├── user-app/ Vue 3 + Vite 用户端 H5 +├── admin/ Vue 3 + Vite 运营管理后台 +├── deploy/ 开发依赖与 Nginx 示例,生产脚本尚不完整 +├── docs/ 原系统 A/B、AI 生产经验、教程和成本资料 +├── data/ 项目内容、剧本、分镜与生成过程资料 +├── storage/ 当前本地私有素材存储 +└── tmp/ 临时处理文件 +``` + +### 2.2 运行拓扑 + +```mermaid +flowchart TB + User[用户端 Vue H5] --> Nginx[Nginx] + Admin[管理端 Vue H5] --> Nginx + Nginx --> API[NestJS API :3010] + API --> MySQL[(MySQL ai_manga)] + API --> Redis[(Redis)] + API --> Storage[本地私有存储\nMinIO 可选] + API --> Providers[AI Providers] + API --> FFmpeg[FFmpeg / FFprobe] + API --> BullMQ[BullMQ 14 队列] + BullMQ --> Worker[独立 Worker\n并发 2] + Worker --> InternalAPI[受 WORKER_SECRET 保护的内部接口] + InternalAPI --> API +``` + +### 2.3 后端基础设施 + +- 框架:NestJS 10。 +- ORM:Prisma 6,MySQL。 +- 队列:BullMQ 5 + Redis。 +- 文件:本地私有文件为当前主存储,MinIO 为可选实现。 +- 媒体:FFmpeg、FFprobe。 +- 文件解析:PDF、DOCX、纯文本。 +- 鉴权:JWT。 +- 后台权限:`admin`、`operator`、`finance`、`auditor` 角色与细粒度 permission。 +- API 前缀:`/api`。 +- 请求体上限:当前 160 MB。 +- 中间件:请求 ID、安全响应头、HTTPS/代理判断、应用层加密请求处理。 +- API 返回:统一 envelope 和统一异常过滤。 + +### 2.4 当前部署事实 + +- 后端由 `ai-backend.service` 托管。 +- Worker 由 `ai-workers.service` 托管。 +- Nginx 分别托管用户端与管理端,并反向代理 `/api/`。 +- MySQL、Redis 为本机服务。 +- 当前 `NODE_ENV=production`、`STORAGE_DRIVER=local`、Worker 并发为 2。 +- 后端没有强制 `HTTPS_REQUIRED`;传输安全依赖 Nginx 与部署配置。 +- `PUBLIC_ASSET_BASE_URL` 类配置当前未设置,依赖外部 Provider 获取临时素材 URL 的链路需要专项复核。 + +## 3. 已实现模块 + +### 3.1 后端模块 + +当前 `AppModule` 注册的业务模块: + +| 模块 | 主要职责 | 状态 | +| --- | --- | --- | +| `AuthModule` | 注册、登录、JWT、账号状态 | 已实现且验证 | +| `UsersModule` | 用户资料 | 已实现 | +| `BillingModule` | 额度、订单、冻结、释放、后台人工调整 | 已实现但支付仅为 mock | +| `ProjectsModule` | 项目、作品库、小说库、创意模式、流水线配置 | 已实现 | +| `ProviderLabModule` | Provider 参数试跑与状态轮询 | 已实现 | +| `AssetsModule` | 上传、下载、预览、Range 流、别名、选择状态 | 已实现且有大量真实素材 | +| `NovelsModule` | 导入、解析、阅读、原创小说、Agent 与章节质量流程 | 已实现但测试不足 | +| `StoryBiblesModule` | 故事圣经、制作规则、确认锁定 | 已实现 | +| `CharactersModule` | 项目角色、全局角色、版本、状态、Prompt 审核与优化经验 | 已实现但复杂度较高 | +| `MemoriesModule` | 剧情、角色、伏笔与连续性记忆 | 已实现但测试桩未同步 | +| `EpisodesModule` | 分集计划、请求预览、质量闸门 | 已实现但质量测试有漂移 | +| `ScriptsModule` | 单集剧本、分镜、动态时长、提示词、转场 | 已实现但测试不足 | +| `ImagesModule` | 角色图、锚点图、三视图、关键帧、视觉质检 | 已实现 | +| `LiveActionModule` | 真人短剧准备、关键帧、视频、QC、合并、后期 | 已实现但仍处于生产化磨合 | +| `MediaModule` | TTS、字幕、传统漫剧合成、音频片段重试 | 已实现 | +| `ProvidersModule` | Provider 目录、配置、调用、日志、成本、异步视频轮询 | 已实现且有真实调用 | +| `QueuesModule` | RenderTask、BullMQ、重试、取消、恢复、Worker 委托 | 已实现但执行覆盖不完整 | +| `ReviewsModule` | 文本/素材审核、返修、案例授权 | 已实现但当前数据库尚无审核记录 | +| `AdminModule` | 运营后台聚合 API、审计、配置、Router 审计 | 已实现 | + +`AiRouterModule` 没有作为顶层模块单独注册,但被 `LiveActionModule` 导入并实际用于真人视频路由。当前它不是覆盖全平台所有 AI 请求的统一 Router。 + +### 3.2 用户端模块 + +用户端实际为 Vue 3 单页应用,不是旧文档描述的 uni-app 多路由实现。 + +顶级导航: + +- 创作:新建、提示词直出、API 快测。 +- 工具:独立创作工具。 +- 项目:项目列表、制作台、IP 资产中心、进度。 +- 作品:视频、小说、素材。 +- 任务:运行任务、审核、额度与成本。 +- 我的:账号、角色资产、教程。 + +制作台主流程: + +```text +来源 -> 版权 -> 故事圣经 -> 角色 -> 记忆 -> 分集 +-> 剧本/分镜 -> 真人视频 -> 合成 -> 审核 +``` + +独立创作工具当前状态: + +| 工具 | 状态 | +| --- | --- | +| 人物三视图 | 已开放;支持主锚点参考或输入本次唯一角色描述;生成后可下载和视觉质检 | +| 影视场景专家 | 待接入 | +| 9 宫格分镜 | 待接入 | +| 16 宫格分镜 | 待接入 | +| 25 宫格分镜 | 待接入 | +| 原创剧本 | 待接入 | + +虽然 `user-app/src/pages/` 中保留了多页面文件,但当前 `App.vue` 实际只加载大型 `pages/index/index.vue`,没有 Vue Router。多数业务都集中在一个约 1.35 万行的页面组件中。 + +### 3.3 管理端模块 + +管理端实际为自研 Vue 3 单页后台,不是完整 GeekerAdmin 集成。 + +当前菜单包括: + +- 仪表盘、使用教程。 +- 项目、小说、全局角色、项目角色、分镜、成品。 +- 订单额度、内容审核、任务。 +- Router 审计、爆款诊断、AI 平台入口。 +- Provider、成本、用户、素材。 +- 系统配置、版权、审计日志。 + +后台同样集中在一个约 1.03 万行的 `App.vue` 中,没有实际 Router、Store 和拆分后的 Views。 + +## 4. 已完成功能与完成度 + +| 业务域 | 当前能力 | 完成度判断 | +| --- | --- | --- | +| 账号与安全 | 注册、登录、JWT、账号禁用、密码重置、RBAC、操作审计、应用层加密 | 已实现且验证 | +| 项目 | 新建、列表、删除、取消、进度、作品库、提示词直出分镜 | 已实现 | +| 小说导入 | 粘贴、文件上传、章节切割、编辑、版权确认 | 已实现 | +| 小说阅读 | 目录、分页/翻页、阅读进度、书签、批注 | 已实现 | +| 原创小说 | 创作向导、生成计划、章节 Agent、上下文、质检、修复、版本、批次 | 已实现但自动测试未稳定 | +| 故事与世界观 | StoryBible、WorldBible、制作文本和锁定 | 已实现 | +| 角色资产 | 项目角色、全局演员、外观版本、状态、服装、声音、授权范围 | 已实现 | +| 角色提示词闭环 | Prompt 版本、审核、优化经验、下一次生成吸收 | 已实现 | +| 人物三视图 | 主锚点模式、纯描述覆盖模式、旗舰模板、下载、视觉评分 | 已实现 | +| IP 资产中心 | 场景/道具提取、主资产、版本、Prompt、冲突检查 | 已实现但 UI/测试仍在磨合 | +| 长篇记忆 | 剧情记忆、角色记忆、伏笔线程、连续性检查 | 已实现但测试未同步 | +| 分集 | 动态数量、质量闸门、请求预览与 Prompt 覆盖 | 已实现但当前一条质量测试失败 | +| 剧本 | 剧本生成、人工编辑、确认、请求预览 | 已实现 | +| 分镜 | 动态时长、关键帧/视频 Prompt、前后镜衔接、转场字段、视觉板 | 已实现 | +| 关键帧 | 角色/场景/道具参考、单镜/批量、视觉审核 | 已实现且有真实图片调用 | +| 视频 | 单镜/批量、多候选、首帧/首尾帧/参考图、原生音频 Provider | 已实现且有真实视频调用 | +| 视频 QC | 自动评分、同模型重试、Fallback、人工确认、候选选择 | 已实现但测试和策略仍在变化 | +| 视频合并 | 硬切/转场、画面归一、源音轨、字幕、BGM、SFX、FFmpeg | 已实现但产品质量仍需持续验收 | +| 原创音乐 | 音乐圣经、MusicProvider、BGM 对齐、资产包 | 部分接入,默认关闭 | +| 口型 | LipSync Provider 抽象、策略与桥接 | 部分接入;当前真实 LipSync Provider 全部禁用 | +| 素材库 | 类型、别名、分镜号/范围、选中/候选/淘汰、Range 播放、下载 | 已实现 | +| 队列 | 14 队列、任务幂等、重试、取消、过期恢复、人工介入 | 已实现但部分任务类型无 Worker 执行器 | +| 成本 | Provider 成本规则、调用日志、人民币展示、额度冻结/释放 | 已实现;财务口径仍需统一 | +| 支付 | 套餐、订单模型与 mock-pay | 仅测试,不是生产支付 | +| 内容审核 | 文本、素材、后台复核、案例授权 | API/UI 已实现,但当前 `ContentReview` 为 0 条 | +| 爆款诊断 | 样本、片段、创意模式、项目绑定 | 已实现,属于原设计后的扩展 | + +## 5. 数据库结构 + +### 5.1 总体规模 + +- Prisma Model:61 个。 +- Prisma Enum:0 个。 +- 数据库迁移:33 个,当前全部已部署。 +- 索引、唯一约束和 JSON 字段大量使用。 +- 业务状态主要使用字符串字段,而不是数据库枚举。 + +### 5.2 模型分组 + +#### 用户与项目 + +- `User` +- `UserModelPreference` +- `Project` +- `ProjectPipelineConfig` +- `ProjectCreativePattern` +- `CreativePattern` + +#### 小说与 Agent + +- `NovelSource` +- `NovelChapter` +- `NovelReadingProgress` +- `NovelBookmark` +- `NovelAnnotation` +- `NovelGenerationPlan` +- `NovelChapterVersion` +- `NovelContextMemory` +- `NovelQualityReport` +- `NovelVersionSnapshot` +- `NovelDerivativeJob` +- `AgentPrompt` +- `AgentRun` + +#### 故事、版权与世界观 + +- `CopyrightRecord` +- `StoryBible` +- `WorldBible` + +#### 角色与 IP 资产 + +- `Character` +- `StoryCharacter` +- `CharacterExtractionVersion` +- `GlobalCharacter` +- `GlobalCharacterAsset` +- `GlobalCharacterLookVersion` +- `CharacterImage` +- `CharacterImageQualityReview` +- `CharacterMemory` +- `CharacterDesignVersion` +- `CharacterPromptVersion` +- `CharacterPromptReview` +- `CharacterPromptOptimizationLesson` +- `CharacterState` +- `ActorProfile` +- `ProjectVisualAsset` + +#### 剧集生产 + +- `Episode` +- `EpisodeScript` +- `StoryboardShot` +- `ShotImage` +- `VideoClip` +- `PlotMemory` +- `PlotThread` +- `ContinuityCheck` + +#### 素材、任务与 Provider + +- `Asset` +- `RenderTask` +- `ProviderConfig` +- `ProviderLog` + +#### 计费、审核与运营 + +- `Order` +- `QuotaAccount` +- `QuotaLog` +- `RevisionRequest` +- `ContentReview` +- `CaseShowcase` +- `HitAnalysisCase` +- `HitAnalysisSegment` +- `AnalyticsEvent` +- `SystemConfig` +- `OperationLog` + +### 5.3 当前数据库数据快照 + +| 数据 | 数量 | +| --- | ---: | +| 用户 | 2 | +| 项目 | 20 | +| 小说源 | 10 | +| 小说章节 | 256 | +| 故事圣经 | 13 | +| 项目角色 | 87 | +| 全局角色 | 14 | +| 分集 | 87 | +| 单集剧本 | 21 | +| 分镜 | 114 | +| 素材 | 977 | +| 任务 | 748 | +| Provider 配置 | 112 | +| Provider 调用日志 | 1002 | +| 内容审核记录 | 0 | +| 操作审计记录 | 44 | + +素材构成: + +| 类型 | 数量 | +| --- | ---: | +| image | 600 | +| video_clip | 215 | +| video | 74 | +| audio | 50 | +| subtitle | 33 | +| 其他角色图、片段音频、成品类型 | 5 | + +任务状态: + +| 状态 | 数量 | +| --- | ---: | +| success | 646 | +| failed | 77 | +| manual_required | 22 | +| pending | 2 | +| completed | 1 | + +`completed` 不在当前 `TASK_STATUSES` 常量中,属于历史状态漂移,需要迁移或兼容处理。 + +## 6. API 接口现状 + +### 6.1 总量 + +- Controller:25 个。 +- HTTP 方法装饰器:289 个。 +- API 全局前缀:`/api`。 +- 当前没有生成 OpenAPI/Swagger 规范,接口事实依赖 Controller、DTO 与前端 client。 + +### 6.2 分组 + +| 接口域 | 代表能力 | 路由规模 | +| --- | --- | ---: | +| Admin | 仪表盘、项目、用户、素材、作品、Provider、成本、审计、Router | 47 | +| Characters | 项目角色、全局角色、版本、状态、IP 资产、Prompt、声音 | 47 | +| Projects | 项目、库、阅读入口、创意模式、流水线配置 | 28 | +| Live Action | 角色档案、关键帧、视频、QC、候选、渲染、音乐 | 21 | +| Novel Generation | 生成计划、IP 圣经、章节、批次、暂停恢复 | 17 | +| Providers | 目录、配置、执行、日志、成本、偏好 | 16 | +| Billing | 套餐、订单、额度、冻结/释放、后台调整 | 13 | +| Scripts | 剧本、分镜、请求预览、确认、重生 | 12 | +| Memories | 剧情、角色、伏笔、上下文、连续性 | 11 | +| Images | 角色图、关键帧、锚点、视觉质检 | 9 | +| Queues | 创建、列表、重试、取消、恢复、统计 | 9 | +| Reviews | 文本/素材审核、案例授权、后台复核 | 9 | +| Novels / Wizard / Original | 导入、解析、原创、创作向导 | 16 | +| Assets | 上传、预览、Range 下载、临时 URL | 7 | +| Auth / Crypto | 登录注册、用户信息、加密会话 | 7 | +| Episodes / StoryBible / Media | 分集、故事圣经、音频字幕视频 | 14 | + +### 6.3 安全边界 + +- 业务接口主要使用 JWT Guard。 +- 管理能力在 Service 层执行 permission 检查。 +- Worker 内部接口使用 `WORKER_SECRET` 和 timing-safe 比较。 +- 生产环境缺失 Worker secret 时不会使用本地默认值。 +- 素材为私有存储,支持鉴权下载、Range 请求和带签名的临时 URL。 +- API Key 经服务端密钥加密存储,后台不回显明文。 + +## 7. 前后端目录结构 + +### 7.1 后端 + +```text +backend/src/ +├── admin/ +├── ai-router/ +├── assets/ +├── auth/ +├── billing/ +├── characters/ +├── common/ +├── config/ +├── episodes/ +├── images/ +├── live-action/ +├── media/ +├── memories/ +├── novels/ +├── prisma/ +├── projects/ +├── provider-lab/ +├── providers/ +├── queues/ +├── reviews/ +├── scripts/ +├── story-bibles/ +└── users/ +``` + +当前后端约 9.39 万行 TypeScript。最大文件: + +- `live-action.service.ts`:约 1.42 万行。 +- `characters.service.ts`:约 7443 行。 +- `providers.service.ts`:约 6776 行。 +- `admin.service.ts`:约 6096 行。 +- `scripts.service.ts`:约 4600 行。 + +### 7.2 用户端 + +```text +user-app/src/ +├── api/ +├── components/tools/CreativeTools.vue +├── pages/index/index.vue +├── pages/auth/ +├── pages/help/ +├── pages/projects/ +├── pages/user/ +├── App.vue +├── workflow.ts +└── styles.css +``` + +当前约 2.37 万行,主要功能集中在 `index.vue`、`api/client.ts` 和全局 CSS。 + +### 7.3 管理端 + +```text +admin/src/ +├── api/client.ts +├── api/crypto.ts +├── App.vue +├── main.ts +└── styles.css +``` + +`router/`、`stores/`、`views/` 当前只有占位文件。管理端约 1.34 万行。 + +### 7.4 Worker + +```text +workers/src/main.ts +``` + +每个队列由 BullMQ Worker 消费,任务本体通过内部 HTTP 接口委托后端执行,Worker 本身不重复实现业务逻辑。 + +## 8. 当前 AI 调用流程 + +### 8.1 通用 Provider 流程 + +```mermaid +flowchart LR + Input[业务输入] --> Preference[用户/项目模型偏好] + Preference --> Config[ProviderConfig] + Config --> Limit[启停、优先级、成本上限] + Limit --> Execute[ProvidersService] + Execute --> Sync[同步文本/图片/音频] + Execute --> Async[异步视频提交与轮询] + Sync --> Log[ProviderLog] + Async --> Log + Log --> Asset[Asset / VideoClip] + Log --> Task[RenderTask 状态与成本] + Execute --> Fallback[失败回退或人工介入] +``` + +### 8.2 Provider 现状 + +- Provider 配置共 112 个。 +- Mock 配置 11 个,其中 10 个启用、1 个 LipSync mock 关闭。 +- Real 配置 101 个,其中 64 个启用、37 个关闭。 +- 当前已启用的真实 Provider 覆盖 Text、Novel、Image、Video、Voice、Music、Embedding、Moderation。 +- 真实 LipSync Provider 当前全部关闭。 +- 代表性已启用模型族:OpenAI、豆包/火山、DeepSeek、Qwen、Kimi、智谱、MiniMax、Kling、Seedance、Seedream、Sora 等。 +- 全局默认仍为 mock,显式选择真实 Provider 时可产生真实调用。 + +Provider 日志当前共 1002 条: + +| 状态 | 数量 | +| --- | ---: | +| success | 825 | +| failed | 170 | +| running | 5 | +| cancelled | 2 | + +数据库 `cost_actual` 原始累计值为 354.9416。由于 Provider 成本币种和历史规则可能不同,该值只能用于技术对账,不能直接视为财务账单。 + +近期真实使用主要集中在: + +- `openai-image`。 +- `kling-v3-native-audio-720p-video`。 +- `openai-responses-text`。 +- `volcengine_seedance_20_mini`。 +- `volcengine-seedream-50-image`。 +- `openai-tts`。 + +### 8.3 真人短剧流程 + +```text +剧本/外部提示词 +-> 动态分镜与前后镜关系 +-> 角色/场景/道具资产计划 +-> Prompt Engine +-> 关键帧 +-> 视频 Provider 预检与成本估算 +-> 单镜或批量视频、多候选 +-> 自动 QC / 重试 / Fallback / 人工选择 +-> Scene Composer 计划 +-> 字幕 / 源音轨 / TTS / BGM / SFX / 转场 +-> FFmpeg 合成 +-> 成品与素材库 +``` + +当前支持的后期参数包括:源音轨、额外音频、字幕、BGM、SFX、环境音、无源音轨回退 SFX、口型、字幕模式和音量控制。原创音乐与 Scene Composer 由项目配置控制,默认并非全部开启。 + +### 8.4 小说流程 + +```text +创作 Brief / 导入小说 +-> 生成计划 +-> IP/故事/世界观规则 +-> Agent Prompt +-> 章节草稿 +-> 质量报告 +-> 自动修复或重写 +-> 章节版本与上下文记忆 +-> 小说快照 +-> 听书/短剧派生任务 +``` + +小说新引擎代码已形成,但目前自动测试和管理体验还没有达到可无人值守批量生产的程度。 + +## 9. 队列任务流程 + +### 9.1 队列与任务 + +BullMQ 队列共 14 个: + +```text +novel_queue +parse_queue +story_queue +character_queue +episode_queue +script_queue +storyboard_queue +image_queue +audio_queue +subtitle_queue +video_queue +qc_queue +review_queue +analytics_queue +``` + +任务类型共 21 个,状态包括: + +```text +pending / running / success / failed / retrying +cancelled / manual_required / skipped +``` + +### 9.2 执行链 + +```mermaid +flowchart LR + API[API 创建 RenderTask] --> Idempotency[输入哈希 / 幂等键] + Idempotency --> Queue[BullMQ 入队] + Queue --> Worker[独立 Worker] + Worker --> Secret[WORKER_SECRET] + Secret --> Delegate[backend internal/worker execute] + Delegate --> Provider[通用 Provider 执行] + Delegate --> Live[真人视频业务执行] + Provider --> Result[日志 / 素材 / 状态] + Live --> Result + Result --> Retry[按类型自动重试] + Retry --> Manual[耗尽后 manual_required] +``` + +已实现: + +- 幂等键与输入哈希。 +- 不同任务类型的默认重试次数。 +- 任务取消与队列 Job 清理。 +- stale running 任务恢复。 +- 失败后自动重试与人工介入。 +- 队列统计。 + +### 9.3 当前队列覆盖缺口 + +任务目录比 Worker 执行器更宽。以下任务在当前 `providerTypeForTask` 中没有实际 Provider 映射,也不属于真人业务执行器,走通用 Worker 时会被标记为 `skipped`: + +- `long_memory_generate` +- `subtitle_generate` +- `live_action_keyframe_generate` +- `live_action_video_render` +- `analytics_event` + +其中部分能力目前由同步 Service 方法直接执行,因此功能本身不一定不可用;但“全部已队列化”的说法不准确。`video_render` 在通用任务映射中指向 `VideoProvider`,与 FFmpeg 合成语义也需要重新核对。 + +## 10. 已实现但未完整写入原设计文档的功能 + +相较 `docs/system_a`、`docs/system_b` 的早期设计,当前代码额外出现或显著深化了: + +1. 小说创作向导、章节 Agent、上下文构建、质量修复、版本快照与派生任务。 +2. 小说阅读器、书签、批注、阅读进度。 +3. 全局角色资产、外观版本、角色状态、角色 Prompt 版本与审核经验闭环。 +4. 人物三视图独立工具,以及“主锚点参考/本次描述覆盖”两种模式。 +5. 三视图视觉质量评分、问题提取、优化经验进入下一次生成。 +6. 项目级场景/道具/IP 资产中心和主资产版本。 +7. 动态分镜时长、前后镜衔接、转场字段、请求预览与 Prompt 人工覆盖。 +8. 真人短剧 AI Router、镜头评分、成本预检、候选视频与自动质量闭环。 +9. Kling 原生音频、Seedance、Seedream、Hailuo、Sora 等多模型目录。 +10. Scene Composer、字幕/BGM/SFX/环境音开关、源音轨策略和原创音乐包。 +11. 素材别名、选中/候选/淘汰状态、按分镜号和范围联合筛选。 +12. Provider Lab、模型偏好、人民币成本标签和 Provider 调用审计。 +13. 爆款诊断、拉片片段、创意模式和项目模式绑定。 +14. API 应用层加密、临时素材签名 URL、视频 Range 流式播放。 + +## 11. 与原设计文档的主要差异 + +| 领域 | 原设计世界 | 当前代码现实 | +| --- | --- | --- | +| 产品定位 | 系统 A 小说转漫剧 + 系统 B 真人写真 | 已合并演化为小说、短剧、视频、角色/IP 资产、创作工具平台 | +| 用户端 | uni-app 多页面 | Vue 3 + Vite H5,核心集中在单一大页面 | +| 管理端 | GeekerAdmin | 自研 Vue 单页后台,未引入完整 GeekerAdmin 架构 | +| 数据库 | 约 20 个核心表 | 61 个 Model、33 个迁移 | +| Provider | 抽象层与少量真实模型 | 112 个配置,覆盖大量国内外文本/图像/视频/声音模型 | +| AI Router | 后续路由能力 | 已用于真人视频,但未成为全平台统一入口 | +| 小说 | 原创 mock + 导入改编 | 已加入 Agent、质量循环、版本、阅读器和派生任务 | +| 分镜 | 固定镜头/宫格倾向 | 动态时长、上下镜关系、首尾帧/多图、转场与声音层 | +| 视频 | 图片、TTS、字幕、简单合成 | 真人视频候选、QC、原生音频、Scene Composer、复杂 FFmpeg 后期 | +| 队列 | 所有生产任务统一异步 | Worker 已运行,但部分声明任务没有异步执行器,部分流程仍同步 | +| 存储 | MinIO 优先设计 | 当前实际使用本地私有存储,MinIO 未完整配置 | +| 支付 | 订单支付闭环 | 额度与 mock-pay 可用,真实支付未接入 | +| 审核 | 内容审核闭环 | API/UI 已有,但数据库没有实际审核记录 | +| 测试 | 文档定义生产验收 | 后端单测有覆盖但当前 35 条失败,前后端无自动测试 | +| 版本管理 | 设计文档持续推进 | Git 只有初始提交,大量现实代码尚未形成版本基线 | + +## 12. 当前待优化问题 + +### P0:必须先处理 + +#### 12.1 Git 无法代表当前系统 + +- `main` 只有 1 个提交。 +- 当前有 85 个修改项、47 个未跟踪项。 +- 多个数据库迁移、小说新引擎、Provider Lab、人物三视图、数据与文档都未纳入提交。 + +风险:服务器故障、误操作或换机后,无法从 Git 恢复当前系统。 + +建议:先做数据库与素材备份,再把当前状态拆成可审阅提交;不要把生成素材和临时文件混入源码提交。 + +#### 12.2 测试未达到发布基线 + +后端结果: + +```text +Test Files: 19 passed / 8 failed +Tests: 238 passed / 35 failed +``` + +失败主要分为: + +1. 新增 Prisma 依赖后,旧测试 mock 没有补齐。 +2. 动态分镜、质量闸门、BGM/SFX 策略改变后,旧期望未更新。 +3. Provider 成本标签新增人民币后,断言仍使用旧文案。 +4. Seedance 真人参考图预检策略出现测试与实现不一致。 + +应逐条判断“测试旧了”还是“代码回归”,不能简单批量改断言。 + +#### 12.3 当前运行进程未加载最新构建 + +审计期间全仓构建通过,但 backend 和 worker 进程启动时间早于最新构建产物。需要在备份、测试和变更确认后执行受控重启,并做健康检查与核心链路冒烟测试。 + +### P1:高优先级 + +#### 12.4 队列执行覆盖不完整 + +补齐或明确移出队列:长篇记忆、字幕、真人关键帧、真人合并、分析事件。避免任务显示已入队,Worker 最后却标记 `skipped`。 + +#### 12.5 状态字段漂移 + +数据库中存在历史 `completed`,当前代码只认 `success`。项目、任务、素材、审核等大量状态均为自由字符串。建议建立状态字典、迁移脚本和兼容测试。 + +#### 12.6 本地素材是单机风险点 + +当前 977 个素材使用本地私有存储,MinIO 未完整配置,部署目录中也没有可执行备份脚本。至少需要: + +- 数据库定时备份。 +- `storage/private` 增量备份。 +- 恢复演练。 +- 磁盘容量与失败告警。 + +#### 12.7 任务和调用失败积压 + +- RenderTask:77 failed、22 manual_required、2 pending。 +- ProviderLog:170 failed、5 running。 + +需要区分历史测试垃圾、外部异步任务和真实待处理任务,并提供批量归档/恢复策略。 + +#### 12.8 Provider 配置过多且默认策略不清晰 + +数据库中有 112 个 Provider 配置,但全局默认仍是 mock。真实 Provider 启停、用户偏好、项目偏好、Router 和显式 provider_code 同时存在,容易出现“页面选了 A,实际走 B”的理解成本。 + +建议为每次调用固定记录并展示:请求 Provider、路由原因、实际 Provider、模型、参数、输入参考图、成本和回退链。 + +#### 12.9 外部 Provider 素材 URL 配置需复核 + +当前公开素材基础 URL 未设置,而部分第三方视频/图片 Provider 需要可访问的临时参考图 URL。应把此项纳入 Provider preflight,而不是运行到提交时才失败。 + +#### 12.10 审核闭环尚未实际使用 + +`ContentReview` 当前为 0 条。需要确认是入口未触发、审核结果写到了其他字段,还是生产流程绕过了正式审核。 + +### P2:结构性优化 + +#### 12.11 超大文件与前端单体化 + +- `live-action.service.ts` 约 1.42 万行。 +- 用户端主页面约 1.35 万行。 +- 管理端主页面约 1.03 万行。 + +这会放大回归风险和协作冲突。建议按稳定业务边界逐步拆分,不做一次性大重构。 + +#### 12.12 缺少前后端自动测试与正式 E2E + +用户端、管理端目前没有测试文件。后端以单元测试为主,没有覆盖“登录 -> 项目 -> 分镜 -> 真实 Provider -> 素材 -> 合并”的稳定 E2E。 + +#### 12.13 API 缺少机器可读契约 + +当前 289 个接口依赖 TypeScript DTO 和手写 client,没有 OpenAPI 文档与自动 client 生成。接口数量继续增长后,前后端容易漂移。 + +#### 12.14 生产运维不完整 + +`deploy/` 只有开发 compose、Nginx 示例和说明,没有落地的发布、回滚、备份、日志轮转、监控和告警脚本。当前主要依靠 systemd 日志与人工检查。 + +#### 12.15 真实支付、多租户和财务对账未完成 + +- 支付仍为 mock-pay。 +- 当前是用户级数据隔离,没有组织/租户模型。 +- `cost_actual` 的币种与财务账单口径需统一。 + +## 13. 建议建立的唯一真相机制 + +### 13.1 Work 负责 + +```text +产品定位 +业务规则 +架构决策 +未来路线 +功能设计文档 +``` + +### 13.2 Git + Codex 负责 + +```text +当前代码 +数据库迁移 +实现细节 +测试结果 +部署与运行事实 +``` + +### 13.3 同步规则 + +每个重要功能建议使用以下闭环: + +```text +Work 讨论 +-> FEATURE_xxx_V1.md +-> Codex 实现 +-> 测试与部署 +-> 更新 CHANGELOG.md +-> 更新 PROJECT_STATUS_Vn.md +-> 回灌 Work +``` + +状态文档不应每次全量覆盖,而应保留 V1、V2、V3,便于追踪系统如何演化。 + +## 14. 建议放入 Work 的首批资料 + +```text +AI 内容生产平台/ +├── 01_项目现状/ +│ └── PROJECT_STATUS_V1.md +├── 02_架构设计/ +│ └── SYSTEM_ARCHITECTURE.md +├── 03_小说引擎/ +│ └── NOVEL_ENGINE.md +├── 04_短剧引擎/ +│ └── DRAMA_ENGINE.md +├── 05_AI流水线/ +│ └── AI_PIPELINE.md +├── 06_数据库/ +│ └── DATABASE.md +└── 07_版本记录/ + └── CHANGELOG.md +``` + +本次只生成真实状态基线。其余文档应以本文件为依据重新整理,而不是直接复制旧系统 A/B 文档并假定其全部已实现。 + +## 15. 本次验证记录 + +已执行并确认: + +- 全仓生产构建通过。 +- Prisma schema 可用。 +- 33 个迁移与当前数据库一致。 +- 后端健康检查通过。 +- backend 与 worker systemd 服务正在运行。 +- MySQL、Redis、Nginx 正在运行。 +- 数据库只读数量与 Provider/任务状态统计完成。 +- 自动测试执行完成,并如实记录 35 条失败。 + +未执行: + +- 未重启生产服务。 +- 未修改数据库数据。 +- 未触发新的付费 AI 生成。 +- 未做浏览器端完整 E2E。 +- 未做数据库或素材恢复演练。 + diff --git a/admin/src/App.vue b/admin/src/App.vue index 3d71380..d273494 100644 --- a/admin/src/App.vue +++ b/admin/src/App.vue @@ -1,5 +1,6 @@ + + + + diff --git a/shared/frontend/ai-request-inspector/ai-request-inspector.ts b/shared/frontend/ai-request-inspector/ai-request-inspector.ts new file mode 100644 index 0000000..02c658b --- /dev/null +++ b/shared/frontend/ai-request-inspector/ai-request-inspector.ts @@ -0,0 +1,298 @@ +export type AiRequestInspectionStatus = 'prepared' | 'succeeded' | 'failed'; + +export interface AiRequestInspectionRecord { + id: string; + title: string; + category: 'preview' | 'generation' | 'review' | 'render'; + method: string; + path: string; + status: AiRequestInspectionStatus; + submitted_at: string; + finished_at: string | null; + request_id: string | null; + request_body: unknown; + execution_evidence: unknown; + error_message: string | null; +} + +type InspectionListener = (records: AiRequestInspectionRecord[]) => void; + +const STORAGE_KEY = 'ai_content_ai_request_inspections_v1'; +const MAX_RECORDS = 30; +const listeners = new Set(); +let records: AiRequestInspectionRecord[] = readStoredRecords(); + +const AI_REQUEST_PATTERNS = [ + /\/provider-lab\/runs$/, + /\/prompt-storyboard(?:\/preview)?$/, + /\/original\/(?:idea|outline|chapters|self-check)$/, + /\/production\/prompts\/.+\/preview$/, + /\/production\/contracts\/.+\/ai-review$/, + /\/projects\/.+\/reviews\/text$/, + /\/assets\/.+\/review$/, + /\/admin\/novel-creation-wizard\/proposals\/jobs$/, + /\/novel-generation\/.+\/(?:ip-bible|chapters\/.+\/run|chapters\/batch)\/jobs$/, + /\/admin\/hit-analyses(?:$|\/.+\/(?:analyze|patterns)$)/, + /(?:^|[\/_-])(?:generate|regenerate|retry|extract|optimize|ai-review|preflight|render|compose|synthesize|test|review-visual|quality-check|cost-estimate|request-preview|plan-request-preview|prepare|auto-bind)(?:[\/?_-]|$)/, +]; + +export function isInspectableAiRequest(path: string, method: string) { + if (method === 'HEAD') return false; + if (method === 'GET' && !/\/(?:preflight|cost-estimate)(?:[/?]|$)/.test(path)) return false; + return AI_REQUEST_PATTERNS.some((pattern) => pattern.test(path)); +} + +export function beginAiRequestInspection(input: { + path: string; + method: string; + body: unknown; +}) { + const record: AiRequestInspectionRecord = { + id: createInspectionId(), + title: requestTitle(input.path), + category: requestCategory(input.path), + method: input.method, + path: input.path, + status: 'prepared', + submitted_at: new Date().toISOString(), + finished_at: null, + request_id: null, + request_body: normalizeInspectorValue(buildSubmittedParameters(input.path, input.body)), + execution_evidence: null, + error_message: null + }; + + records = [record, ...records].slice(0, MAX_RECORDS); + publish(); + return record.id; +} + +export function completeAiRequestInspection( + id: string | null, + input: { requestId?: string | null; responseData?: unknown } +) { + if (!id) return; + const record = records.find((item) => item.id === id); + const extractedEvidence = extractExecutionEvidence(input.responseData); + updateInspection(id, { + status: 'succeeded', + finished_at: new Date().toISOString(), + request_id: input.requestId ?? null, + execution_evidence: extractedEvidence ?? ( + record?.category === 'preview' ? normalizeInspectorValue(input.responseData) : null + ), + error_message: null + }); +} + +export function failAiRequestInspection(id: string | null, error: unknown) { + if (!id) return; + updateInspection(id, { + status: 'failed', + finished_at: new Date().toISOString(), + error_message: error instanceof Error ? error.message : String(error ?? '未知错误') + }); +} + +export function subscribeAiRequestInspections(listener: InspectionListener) { + listeners.add(listener); + listener([...records]); + return () => listeners.delete(listener); +} + +export function clearAiRequestInspections() { + records = []; + publish(); +} + +function updateInspection(id: string, patch: Partial) { + records = records.map((record) => record.id === id ? { ...record, ...patch } : record); + publish(); +} + +function publish() { + writeStoredRecords(records); + const snapshot = [...records]; + for (const listener of listeners) listener(snapshot); +} + +function requestCategory(path: string): AiRequestInspectionRecord['category'] { + if (/preview|preflight/.test(path)) return 'preview'; + if (/review|optimize/.test(path)) return 'review'; + if (/render|compose|synthesize/.test(path)) return 'render'; + return 'generation'; +} + +function requestTitle(path: string) { + const rules: Array<[RegExp, string]> = [ + [/provider-lab\/runs$/, 'API 快测'], + [/admin\/providers\/.+\/test$/, '管理端 Provider 实测'], + [/novel-creation-wizard\/proposals\/jobs$/, '小说方案生成'], + [/novel-generation\/.+\/ip-bible\/jobs$/, '小说 IP 圣经生成'], + [/novel-generation\/.+\/chapters\/.+\/jobs$/, '小说章节生成'], + [/admin\/hit-analyses(?:$|\/.+\/(?:analyze|patterns)$)/, '爆款案例 AI 分析'], + [/projects\/.+\/reviews\/text$|assets\/.+\/review$/, '内容 AI 审核'], + [/prompt-storyboard\/preview$/, '提示词分镜预览'], + [/prompt-storyboard$/, '提示词生成分镜'], + [/production\/prompts\/(.+)\/preview$/, 'S+ 阶段 Prompt 预览'], + [/production\/contracts\/.+\/ai-review$/, 'S+ 场景剧本 AI 审核'], + [/request-preview|plan-request-preview/i, 'AI 请求预览'], + [/anchor-video-test/i, '角色元素视频测试'], + [/review-visual/i, '角色图片视觉质检'], + [/quality-check/i, '视频质量检查'], + [/preflight/i, '视频生成预检'], + [/cost-estimate/i, '生成成本估算'], + [/auto-bind/i, '角色音色自动绑定'], + [/character.*(?:image|anchor).*generate|images.*generate/i, '角色资产生成'], + [/keyframe.*generate|generate.*keyframe/i, '关键帧生成'], + [/video.*generate|generate.*video/i, '视频生成'], + [/audio.*generate|voice.*generate|tts/i, '语音生成'], + [/subtitle.*generate/i, '字幕生成'], + [/music.*generate/i, '音乐生成'], + [/story.*generate/i, '故事圣经生成'], + [/episode.*generate/i, '分集计划生成'], + [/script.*generate/i, '剧本生成'], + [/storyboard.*generate/i, '分镜生成'], + [/render|compose/i, '成片合成'], + [/review|optimize/i, 'AI 审核优化'] + ]; + + return rules.find(([pattern]) => pattern.test(path))?.[1] ?? 'AI 请求'; +} + +function buildSubmittedParameters(path: string, body: unknown) { + const queryIndex = path.indexOf('?'); + const query = queryIndex >= 0 ? path.slice(queryIndex + 1) : ''; + const queryParams: Record = {}; + + if (query) { + const search = new URLSearchParams(query); + for (const key of new Set(search.keys())) { + const values = search.getAll(key); + queryParams[key] = values.length > 1 ? values : values[0] ?? ''; + } + } + + return { + path_params: path.slice(0, queryIndex >= 0 ? queryIndex : undefined), + query_params: queryParams, + body: body ?? null + }; +} + +function normalizeInspectorValue(value: unknown, depth = 0, seen = new WeakSet()): unknown { + if (depth > 12) return '[超过展示深度]'; + if (value === null || value === undefined) return value ?? null; + if (typeof value === 'bigint') return value.toString(); + if (typeof value === 'string') return summarizeBinaryString(value); + if (typeof value === 'number' || typeof value === 'boolean') return value; + + if (typeof File !== 'undefined' && value instanceof File) { + return { + file_name: value.name, + mime_type: value.type || 'application/octet-stream', + size_bytes: value.size, + last_modified: value.lastModified + }; + } + if (typeof Blob !== 'undefined' && value instanceof Blob) { + return { mime_type: value.type || 'application/octet-stream', size_bytes: value.size }; + } + if (typeof FormData !== 'undefined' && value instanceof FormData) { + const result: Record = {}; + for (const [key, item] of value.entries()) { + const normalized = normalizeInspectorValue(parseFormValue(key, item), depth + 1, seen); + const current = result[key]; + result[key] = current === undefined + ? normalized + : Array.isArray(current) ? [...current, normalized] : [current, normalized]; + } + return result; + } + if (Array.isArray(value)) { + return value.map((item) => normalizeInspectorValue(item, depth + 1, seen)); + } + if (typeof value === 'object') { + if (seen.has(value)) return '[循环引用]'; + seen.add(value); + const result: Record = {}; + for (const [key, item] of Object.entries(value)) { + result[key] = isSecretKey(key) + ? '[已隐藏敏感字段]' + : normalizeInspectorValue(item, depth + 1, seen); + } + return result; + } + + return String(value); +} + +function parseFormValue(key: string, value: FormDataEntryValue) { + if (typeof value !== 'string') return value; + if (!/(?:_json|params|input)$/i.test(key)) return value; + try { + return JSON.parse(value) as unknown; + } catch { + return value; + } +} + +function summarizeBinaryString(value: string) { + if (/^data:[^;]+;base64,/i.test(value)) { + const commaIndex = value.indexOf(','); + return `[Base64 二进制已省略,共 ${Math.max(0, value.length - commaIndex - 1)} 字符]`; + } + if (value.length > 10_000 && /^[A-Za-z0-9+/=\r\n]+$/.test(value)) { + return `[疑似 Base64 二进制已省略,共 ${value.length} 字符]`; + } + return value; +} + +function isSecretKey(key: string) { + return /(?:password|authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|secret)/i.test(key); +} + +function extractExecutionEvidence(value: unknown) { + if (!value || typeof value !== 'object') return null; + const evidenceKeys = /^(?:task|tasks|input_json|request_preview|request_params|request_params_override|generation_plan|generation_plans|provider|provider_log|route_decision|router_decision|compiled_request|final_request|cost_estimate|estimated_cost)$/i; + const evidence: Record = {}; + + function visit(node: unknown, depth: number, prefix: string) { + if (!node || typeof node !== 'object' || depth > 5) return; + if (Array.isArray(node)) { + node.forEach((item, index) => visit(item, depth + 1, `${prefix}[${index}]`)); + return; + } + for (const [key, item] of Object.entries(node)) { + const path = prefix ? `${prefix}.${key}` : key; + if (evidenceKeys.test(key)) evidence[path] = normalizeInspectorValue(item); + if (!evidenceKeys.test(key)) visit(item, depth + 1, path); + } + } + + visit(value, 0, ''); + return Object.keys(evidence).length > 0 ? evidence : null; +} + +function createInspectionId() { + return `ai_req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; +} + +function readStoredRecords(): AiRequestInspectionRecord[] { + if (typeof window === 'undefined') return []; + try { + const parsed = JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '[]') as unknown; + return Array.isArray(parsed) ? parsed.slice(0, MAX_RECORDS) as AiRequestInspectionRecord[] : []; + } catch { + return []; + } +} + +function writeStoredRecords(nextRecords: AiRequestInspectionRecord[]) { + if (typeof window === 'undefined') return; + try { + window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(nextRecords)); + } catch { + // Inspection history is diagnostic only; generation must not fail if storage is full. + } +} diff --git a/user-app/src/api/ai-request-inspector.spec.ts b/user-app/src/api/ai-request-inspector.spec.ts new file mode 100644 index 0000000..72be55a --- /dev/null +++ b/user-app/src/api/ai-request-inspector.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { + beginAiRequestInspection, + clearAiRequestInspections, + completeAiRequestInspection, + isInspectableAiRequest, + subscribeAiRequestInspections, + type AiRequestInspectionRecord +} from '../../../shared/frontend/ai-request-inspector/ai-request-inspector'; + +describe('AI request inspector route matching', () => { + it.each([ + ['/provider-lab/runs', 'POST'], + ['/projects/p1/reviews/text', 'POST'], + ['/assets/a1/review', 'POST'], + ['/admin/novel-creation-wizard/proposals/jobs', 'POST'], + ['/projects/p1/novel-generation/plans/n1/ip-bible/jobs', 'POST'], + ['/projects/p1/novel-generation/plans/n1/chapters/2/run/jobs', 'POST'], + ['/projects/p1/novel-generation/plans/n1/chapters/batch/jobs', 'POST'], + ['/admin/hit-analyses', 'POST'], + ['/admin/hit-analyses/h1/analyze', 'POST'], + ['/admin/hit-analyses/h1/patterns', 'POST'], + ['/admin/providers/provider-1/test', 'POST'], + ['/projects/p1/story-bible/generate', 'POST'], + ['/projects/p1/characters/extract', 'POST'], + ['/characters/c1/anchor-video-test', 'POST'], + ['/characters/c1/generate-images', 'POST'], + ['/characters/c1/images/i1/review-visual', 'POST'], + ['/projects/p1/episodes/plan-request-preview', 'POST'], + ['/episodes/e1/script/request-preview', 'POST'], + ['/episodes/e1/script/generate', 'POST'], + ['/episodes/e1/audio/segments/2/retry', 'POST'], + ['/episodes/e1/live-action/shots/prepare', 'POST'], + ['/episodes/e1/live-action/video-clips/preflight?provider_code=kling', 'GET'], + ['/episodes/e1/live-action/video-clips/cost-estimate?provider_code=kling', 'GET'], + ['/live-action/video-clips/v1/quality-check', 'POST'], + ['/episodes/e1/live-action/render', 'POST'] + ])('captures %s %s', (path, method) => { + expect(isInspectableAiRequest(path, method)).toBe(true); + }); + + it.each([ + ['/projects/p1', 'PATCH'], + ['/projects/p1/novel-generation/plans', 'POST'], + ['/projects/p1/novel-generation/plans/n1', 'PATCH'], + ['/projects/p1/story-bible/confirm', 'POST'], + ['/characters/c1/set-anchor', 'POST'], + ['/episodes/e1/live-action/shots/s1/prompt', 'POST'], + ['/live-action/video-clips/v1/manual-review', 'POST'], + ['/live-action/video-clips/v1/select-candidate', 'POST'], + ['/assets/a1', 'GET'] + ])('ignores non-AI mutation %s %s', (path, method) => { + expect(isInspectableAiRequest(path, method)).toBe(false); + }); + + it('records path, query and body while redacting secrets', () => { + clearAiRequestInspections(); + let latest: AiRequestInspectionRecord[] = []; + const unsubscribe = subscribeAiRequestInspections((records) => { latest = records; }); + beginAiRequestInspection({ + path: '/episodes/e1/live-action/video-clips/preflight?provider_code=kling&image_id=1&image_id=2', + method: 'GET', + body: { prompt: '测试提示词', api_key: 'must-not-leak' } + }); + + expect(latest[0]?.request_body).toEqual({ + path_params: '/episodes/e1/live-action/video-clips/preflight', + query_params: { provider_code: 'kling', image_id: ['1', '2'] }, + body: { prompt: '测试提示词', api_key: '[已隐藏敏感字段]' } + }); + unsubscribe(); + clearAiRequestInspections(); + }); + + it('keeps the complete response from request previews as execution evidence', () => { + clearAiRequestInspections(); + let latest: AiRequestInspectionRecord[] = []; + const unsubscribe = subscribeAiRequestInspections((records) => { latest = records; }); + const id = beginAiRequestInspection({ + path: '/episodes/e1/script/request-preview', + method: 'POST', + body: { provider_code: 'text-provider' } + }); + completeAiRequestInspection(id, { + requestId: 'req-1', + responseData: { compiled_prompt: '完整剧本提示词', temperature: 0.4 } + }); + + expect(latest[0]?.execution_evidence).toEqual({ + compiled_prompt: '完整剧本提示词', + temperature: 0.4 + }); + unsubscribe(); + clearAiRequestInspections(); + }); +}); diff --git a/user-app/src/api/client.ts b/user-app/src/api/client.ts index 4f9d7b6..beec08b 100644 --- a/user-app/src/api/client.ts +++ b/user-app/src/api/client.ts @@ -3,6 +3,12 @@ import { base64ToBytes, fileToEncryptedUploadPayload } from './crypto'; +import { + beginAiRequestInspection, + completeAiRequestInspection, + failAiRequestInspection, + isInspectableAiRequest +} from '../../../shared/frontend/ai-request-inspector/ai-request-inspector'; export interface ApiEnvelope { code: number; @@ -23,13 +29,17 @@ const ERROR_MESSAGE_LABELS: Record = { REAL_VIDEO_GENERATION_CONFIRMATION_REQUIRED: '真实视频生成会消耗额度,请先勾选确认后再开始。', LIVE_ACTION_SAMPLE_SHOT_NOT_FOUND: '未找到要测试的真人镜头,请刷新后重新选择。', LIVE_ACTION_KEYFRAME_RASTER_REQUIRED: '真实视频 Provider 需要 PNG/JPG/WebP 关键帧,请先生成真实关键帧,不能使用 mock SVG。', + LIVE_ACTION_REAL_IMAGE_PROVIDER_REQUIRED: '真实视频需要真实图片模型生成 PNG/JPG/WebP 关键帧,请先在后台启用 OpenAI Image 或 Seedream 图片 Provider。', LIVE_ACTION_KEYFRAME_TOO_LARGE_FOR_VIDEO_PROVIDER: '关键帧图片太大,视频 Provider 无法接收,请换小一些的关键帧。', LIVE_ACTION_VIDEO_COST_LIMIT_EXCEEDED: '本次片段预估成本超过你设置的单片段上限。', + LIVE_ACTION_SEEDANCE_REAL_PERSON_IMAGE_UNSUPPORTED: '当前项目使用了真人脸包,Seedance 图生视频可能拒绝疑似真人首帧。请切换海螺/可灵,或开启 Seedance 真人首帧实验开关后只做小样测试。', + LIVE_ACTION_SEEDANCE_REAL_PERSON_TEST_ALLOWED: '已开启 Seedance 真人首帧实验开关,系统会放行提交;平台仍可能拒绝或返回失败。', 'asset_id is required': '请先上传或选择一张关键帧图片。', 'Asset belongs to another project': '该素材属于其它项目,不能绑定到当前镜头。', 'Invalid manual review status': '人工验收状态无效,请刷新后重试。', 'VideoProvider did not return video content or downloadable URL': '视频 Provider 没有返回真实视频内容或可下载地址。', 'ImageProvider did not return image content or downloadable URL': '图片 Provider 没有返回真实图片内容,未生成占位图。', + CHARACTER_IMAGE_PROMPT_REVIEW_NOT_APPROVED: '角色锚点 Prompt 质检未通过,系统已停止图片生成以避免浪费成本。请查看任务里的评分、问题和建议,优化后再生成。', 'VoiceProvider did not return audio content or downloadable URL': 'TTS Provider 没有返回真实音频内容,未生成静音占位。', MINIMAX_TTS_EMPTY_AUDIO: 'MiniMax TTS 没有返回音频,请检查 voice_id / group_id / 账号权限后重试。', PayloadTooLargeError: '上传内容太大,请换小文件或联系管理员调高上传限制。', @@ -41,6 +51,14 @@ const ERROR_MESSAGE_LABELS: Record = { function friendlyApiMessage(message?: string) { if (!message) return ''; + if (message.includes('has not activated the model')) { + const model = /model\s+([a-zA-Z0-9_.-]+)/.exec(message)?.[1]; + return `当前火山方舟账号还没开通${model ? `模型 ${model}` : '这个视频模型'},请先到 Ark Console 激活模型服务,或临时切换其它已开通的视频模型。`; + } + if (/input image may contain real person/i.test(message)) { + return 'Seedance 拒绝了这张疑似真人首帧。这个项目用了真人脸包,建议切换海螺/可灵等支持真人首帧的模型;如果必须用 Seedance,请改用非真人感/偏CG关键帧。'; + } + if (message.startsWith('MINIMAX_TTS_PROVIDER_REJECTED')) { return `MiniMax TTS 拒绝请求:${message.replace(/^MINIMAX_TTS_PROVIDER_REJECTED:\s*/, '')}`; } @@ -51,6 +69,34 @@ function friendlyApiMessage(message?: string) { return ERROR_MESSAGE_LABELS[message] ?? message; } +function gatewayResponseMessage(status: number, contentType: string, text: string) { + const looksLikeHtml = contentType.includes('text/html') || text.trimStart().startsWith('<'); + if (!looksLikeHtml) return ''; + + if (status === 504) { + return '请求超过网关等待时间,但后端可能仍在继续处理。请稍后刷新进度查看结果。'; + } + if (status === 502 || status === 503) { + return '服务连接中断,后台任务可能已提交但还没返回。请先刷新查看任务和素材,不要马上重复点击;确认没有新素材后再重新生成。'; + } + + return `服务返回了非 JSON 响应,请稍后刷新重试。HTTP ${status}`; +} + +function parseJsonPayload(text: string, status: number, contentType: string) { + if (!text) return null; + + try { + return JSON.parse(text) as unknown; + } catch (error) { + const gatewayMessage = gatewayResponseMessage(status, contentType, text); + if (gatewayMessage) { + throw new Error(gatewayMessage); + } + throw error; + } +} + export interface UserProfile { id: string; email: string; @@ -85,11 +131,214 @@ export interface SafeProject { payment_status: string; quality_level: string | null; is_long_series: boolean; + engine_version: string; + production_lifecycle: string; created_at: string; updated_at: string; completed_at: string | null; } +export interface SafeProductionSourceSnapshot { + id: string; + project_id: string; + novel_source_id: string | null; + snapshot_version: number; + source_type: string; + title: string | null; + content_hash: string; + character_count: number; + chapter_manifest_json: unknown; + metadata_json: unknown; + created_by_user_id: string | null; + created_at: string; +} + +export interface ProductionQualityIssue { + code: string; + field_path: string; + message: string; + severity: 'minor' | 'major' | 'fatal'; +} + +export interface ProductionQualityResult { + schema_version: string; + hard_gate_status: 'pass' | 'blocked'; + hard_gate_issues: ProductionQualityIssue[]; + dimension_scores: Record; + weighted_score: number; + confidence: number; + fatal_issues: ProductionQualityIssue[]; + repair_instructions: Array<{ field_path: string; instruction: string }>; + rubric_version: string; +} + +export interface SafeProductionContract { + id: string; + project_id: string; + contract_type: 'source_analysis' | 'adaptation_bible' | 'episode_plan' | 'scene_script' | 'asset_plan' | 'scene_geography'; + schema_version: string; + version: number; + scope_key: string; + parent_contract_id: string | null; + source_snapshot_id: string | null; + input_hash: string; + input_snapshot_json: unknown; + payload_json: Record; + source_refs_json: unknown; + quality_result_json: ProductionQualityResult | null; + reviewer_delta_json: unknown; + status: string; + downstream_status: string; + created_by_user_id: string | null; + confirmed_at: string | null; + released_at: string | null; + created_at: string; + updated_at: string; +} + +export interface SafeProductionContractReview { + id: string; + project_id: string; + contract_id: string; + reviewer_type: string; + reviewer_version: string; + review_no: number; + passed: boolean; + quality_score: number | null; + issues_json: unknown; + delta_json: unknown; + provider_log_id: string | null; + created_by_user_id: string | null; + created_at: string; +} + +export interface SplusSceneScriptReviewIssue { + code: string; + severity: 'minor' | 'major' | 'fatal'; + field_path: string; + scene_id: string | null; + message: string; + evidence: string; + repair_instruction: string; +} + +export interface SplusSceneScriptReviewReport { + schema_version: 'splus_scene_script_review_v1'; + contract_id: string; + reviewer_version: string; + verdict: 'pass' | 'revise' | 'reject'; + overall_score: number; + dimension_scores: Record; + strengths: string[]; + issues: SplusSceneScriptReviewIssue[]; + scene_reviews: Array<{ scene_id: string; score: number; strengths: string[]; issues: string[] }>; + rewrite_priorities: Array<{ priority: number; field_path: string; instruction: string }>; + reviewer_summary: string; + splus_gate: { threshold: number; passed: boolean; blocking_reasons: string[] }; +} + +export interface ProductionStagePromptPreview { + stage: 'source' | 'adaptation' | 'episode' | 'script' | 'assets' | 'geography'; + schema_version: string; + source_snapshot_id: string | null; + parent_contract_id: string | null; + prompt: string; +} + +export interface FlowTestGenerationPlanCompileResult { + compile_version: string; + execution_mode: 'flow_test'; + execution_allowed: false; + paid_ai_call_performed: false; + project_id: string; + episode_id: string; + asset_plan_contract_id: string; + scene_geography_contract_id: string; + generation_plan_count: number; + character_identity_bindings: Array<{ + requirement_id: string; + requirement_name: string; + logical_character_id: string | null; + asset_id: string | null; + character_id: string | null; + character_name: string | null; + status: 'bound_for_flow_test' | 'missing'; + }>; + missing_requirements: Array>; + quality_waiver: { + scope: string; + reason: string; + formal_generation_gate_bypassed: false; + }; + generation_plans: SafeShotGenerationPlan[]; + next_step: string; +} + +export interface SafeProjectPipelineConfig { + id: string; + project_id: string; + ip_isolation_enabled: boolean; + video_engine_enabled: boolean; + scene_composer_enabled: boolean; + default_generation_mode: string; + video_engine_version: string; + scene_composer_version: string; + ip_rules_json: unknown; + video_engine_config_json: unknown; + scene_composer_config_json: unknown; + created_at: string; + updated_at: string; +} + +export interface PromptStoryboardResult { + project: SafeProject; + episode: SafeEpisode; + script: SafeEpisodeScript; + shots: SafeStoryboardShot[]; + character_id?: string; + next_step: string; +} + +export interface PromptStoryboardPreviewShot { + shot_no: number; + scene_name: string; + visual_desc: string; + action_desc: string; + camera_motion: string; + effect_type: string; + duration: number; + route_tier: string; + prompt_text: string; + negative_prompt: string; +} + +export interface PromptStoryboardPreview { + parse_mode: string; + requested_shot_count: number; + detected_shot_count: number | null; + shot_count: number; + target_duration: number; + warnings: string[]; + shots: PromptStoryboardPreviewShot[]; +} + +export interface SafeAssetGeneration { + source: string; + display_name: string | null; + provider_id: string | null; + provider_type: string | null; + provider_code: string | null; + provider_name: string | null; + model_name: string | null; + task_id: string | null; + task_type: string | null; + provider_request_id: string | null; + clip_id: string | null; + status: string | null; + cost_actual: string | null; + created_at: string | null; +} + export interface SafeAsset { id: string; user_id: string | null; @@ -102,9 +351,14 @@ export interface SafeAsset { duration: string | null; size: string | null; hash: string | null; + display_name: string | null; + selection_status: string; + selection_note: string | null; + metadata_json: unknown; visibility: string; status: string; created_at: string; + generation?: SafeAssetGeneration | null; } export interface SafeNovelSource { @@ -118,6 +372,13 @@ export interface SafeNovelSource { chapter_count: number | null; parse_status: string; parse_report: Record | null; + ip_bible_json?: Record | null; + design_json?: Record | null; + volume_plan_json?: Record | null; + ai_provider_code?: string | null; + ai_model_name?: string | null; + ai_cost_estimate?: number | null; + ai_cost_actual?: number | null; created_at: string; } @@ -125,16 +386,135 @@ export interface SafeNovelChapter { id: string; project_id: string; novel_source_id: string | null; + volume_no: number | null; + volume_title: string | null; chapter_no: number; title: string | null; content: string; summary: string | null; visual_summary: string | null; + outline_json?: Record | null; + analysis_json?: Record | null; word_count: number | null; + ai_provider_code?: string | null; + ai_model_name?: string | null; + ai_cost_estimate?: number | null; + ai_cost_actual?: number | null; status: string; created_at: string; } +export interface LibraryAssetRow { + asset: SafeAsset; + project: SafeProject | null; + contexts?: Array>; + alias?: string | null; +} + +export interface ProjectAssetLibraryResult { + assets: LibraryAssetRow[]; + total: number; + limit: number; + page: number; + total_pages: number; +} + +export interface LibraryNovelRow { + source: SafeNovelSource; + project: SafeProject | null; + chapters: SafeNovelChapter[]; + chapter_count: number; +} + +export interface UserLibraryResult { + projects: SafeProject[]; + videos: LibraryAssetRow[]; + assets: LibraryAssetRow[]; + novels: LibraryNovelRow[]; +} + +export interface LibraryNovelDetail { + source: SafeNovelSource; + project: SafeProject; + recent_chapters: SafeNovelChapter[]; + first_chapter_id: string | null; +} + +export interface LibraryNovelChaptersResult { + source: SafeNovelSource; + project: SafeProject; + chapters: SafeNovelChapter[]; + total: number; + limit: number; + page: number; + total_pages: number; +} + +export interface LibraryChapterNav { + id: string; + chapter_no: number; + title: string | null; +} + +export interface LibraryNovelChapterResult { + source: SafeNovelSource; + project: SafeProject; + chapter: SafeNovelChapter; + previous_chapter: LibraryChapterNav | null; + next_chapter: LibraryChapterNav | null; +} + +export interface NovelReadingProgress { + id: string; + user_id: string; + novel_source_id: string; + chapter_id: string; + page_index: number; + page_count: number; + progress_percent: string | null; + updated_at: string; +} + +export interface NovelBookmark { + id: string; + user_id: string; + novel_source_id: string; + chapter_id: string; + page_index: number; + title: string | null; + note_text: string | null; + chapter: LibraryChapterNav | null; + created_at: string; +} + +export interface NovelAnnotation { + id: string; + user_id: string; + novel_source_id: string; + chapter_id: string; + page_index: number; + selected_text: string; + note_text: string | null; + color: string | null; + chapter: LibraryChapterNav | null; + created_at: string; + updated_at: string; +} + +export interface NovelSearchResult { + source: SafeNovelSource; + project: SafeProject; + query: string; + results: Array<{ + chapter: SafeNovelChapter; + snippet: string; + }>; + total: number; + limit: number; + page: number; + total_pages: number; +} + export interface SafeStoryBible { id: string; project_id: string; @@ -147,6 +527,8 @@ export interface SafeStoryBible { world_summary: string | null; ending_direction: string | null; taboo_rules: string | null; + production_bible_json: unknown; + quality_report_json: unknown; version: number; status: string; created_at: string; @@ -156,7 +538,10 @@ export interface SafeStoryBible { export interface SafeCharacter { id: string; project_id: string; + story_character_id?: string | null; global_character_id: string | null; + global_character_look_version_id?: string | null; + global_character_asset_id?: string | null; name: string; alias_names?: unknown; role_type: string; @@ -176,18 +561,252 @@ export interface SafeCharacter { character_arc: string | null; negative_rules?: string | null; anchor_asset_id: string | null; + purpose?: string; wardrobe_variant?: string | null; voice_provider_code?: string | null; voice_model?: string | null; voice_id?: string | null; voice_style?: string | null; performance_style?: string | null; + inherit_voice_from_global?: boolean; + inherit_digital_human_from_global?: boolean; importance_level: number; status: string; created_at: string; updated_at: string; } +export interface SafeGlobalCharacter { + id: string; + name: string; + display_name: string | null; + role_archetype: string; + gender_label: string | null; + age_group: string | null; + identity_desc: string | null; + appearance_desc: string | null; + face_desc: string | null; + hair_desc: string | null; + eye_desc: string | null; + body_desc: string | null; + default_costume_rules: string | null; + special_props: string | null; + personality_desc: string | null; + speech_style: string | null; + voice_provider_code: string | null; + voice_model: string | null; + voice_id: string | null; + voice_style: string | null; + performance_style: string | null; + negative_rules: string | null; + anchor_asset_id: string | null; + voice_sample_asset_id: string | null; + commercial_status: string; + usage_scope: string; + status: string; + created_by_user_id: string | null; + created_at: string; + updated_at: string; +} + +export interface SafeGlobalCharacterAsset { + id: string; + global_character_id: string; + look_version_id?: string | null; + asset_id: string | null; + asset_type: string; + source_type?: string; + source_label?: string | null; + label: string | null; + prompt_text: string | null; + negative_prompt?: string | null; + model_name?: string | null; + seed?: string | null; + resolution?: string | null; + aspect_ratio?: string | null; + cost_estimate?: string | null; + cost_actual?: string | null; + quality_score?: string | null; + consistency_score?: string | null; + face_similarity_score?: string | null; + license_status?: string; + commercial_allowed?: boolean; + review_status?: string; + reviewer_note?: string | null; + is_primary: boolean; + usage_count?: number; + metadata_json?: unknown; + status: string; + created_at: string; + asset?: SafeAsset | null; +} + +export interface SafeCharacterProviderBinding { + id: string; + global_character_id: string; + look_version_id: string | null; + source_asset_id: string | null; + provider_code: string; + provider_asset_type: string; + provider_element_id: string; + element_name: string; + element_description: string | null; + source_duration: string | null; + voice_bound: boolean; + voice_id: string | null; + voice_description: string | null; + binding_version: number; + validation_score: string | null; + validation_report_json: unknown; + status: string; + is_primary: boolean; + created_by_user_id: string | null; + created_at: string; + updated_at: string; + source_asset?: SafeAsset | null; +} + +export interface SafeGlobalCharacterLookVersion { + id: string; + global_character_id: string; + version_name: string; + style_type: string | null; + appearance_desc: string | null; + hair_desc: string | null; + makeup_desc: string | null; + body_desc: string | null; + costume_rules: string | null; + color_palette: string | null; + key_props: string | null; + negative_rules: string | null; + main_anchor_asset_id: string | null; + status: string; + quality_score: string | null; + reviewer_comment: string | null; + source_project_id: string | null; + created_by_user_id: string | null; + created_at: string; + updated_at: string; +} + +export interface MyGlobalCharacterRow { + character: SafeGlobalCharacter; + look_versions?: SafeGlobalCharacterLookVersion[]; + assets: SafeGlobalCharacterAsset[]; + provider_bindings?: SafeCharacterProviderBinding[]; +} + +export interface SafeVisualAnchorCard { + prompt_engine_version?: string; + scope: string; + id: string; + title: string; + character_name: string; + look_version_id: string | null; + look_version_name: string | null; + prompt: string; + negative_prompt: string; + copy_text: string; + sections: Array<{ + title: string; + items: Array<{ label: string; value: string }>; + }>; + prompt_layers?: Array<{ + layer_code: string; + title: string; + asset_type: string; + purpose: string; + candidate_count: number; + prompt: string; + negative_prompt: string; + channels: { + chatgpt_web: string; + openai_api: string; + liblib: string; + midjourney: string; + video_reference: string; + }; + }>; + prompt_quality_checks?: Array<{ + code: string; + severity: 'info' | 'warning'; + message: string; + suggestion?: string; + }>; + prompt_quality_gate?: { + version: string; + mode: string; + status: string; + summary: string; + lock_profile: Array<{ + priority: string; + code: string; + label: string; + status: string; + rule: string; + }>; + priority_rules: string[]; + auto_fix_policy: string[]; + counts: { + warning: number; + info: number; + }; + }; + active_prompt_versions?: SafeCharacterPromptVersion[]; + suggested_candidate_count: number; + next_step: string; +} + +export interface SafeCharacterPromptVersion { + id: string; + project_id: string | null; + character_id: string | null; + global_character_id: string | null; + look_version_id: string | null; + version_no: number; + layer_code: string; + channel: string; + title: string | null; + source_type: string; + source_label: string | null; + prompt_engine_version: string | null; + prompt_text: string; + negative_prompt: string | null; + model_name: string | null; + usage_note: string | null; + quality_score: string | null; + review_comment: string | null; + is_active: boolean; + metadata_json: unknown; + created_by_user_id: string | null; + status: string; + created_at: string; + updated_at: string; +} + +export interface SafeCharacterExtractionVersion { + id: string; + project_id: string; + story_bible_id: string | null; + provider_log_id: string | null; + source_type: string; + source_label: string | null; + prompt_category: string | null; + prompt_version: string | null; + prompt_text: string | null; + provider_code: string | null; + model_name: string | null; + raw_text: string | null; + role_pool_plan_json: unknown; + characters_json: unknown; + quality_score: number | null; + review_comment: string | null; + status: string; + created_by_user_id: string | null; + created_at: string; + updated_at: string; +} + export interface SafeCharacterImage { id: string; project_id: string; @@ -197,16 +816,146 @@ export interface SafeCharacterImage { prompt_text: string | null; negative_prompt: string | null; is_anchor: boolean; + prompt_quality_score: number | null; quality_score: number | null; status: string; created_at: string; asset?: SafeAsset | null; + visual_review?: { + id: string; + score: number | null; + grade: string | null; + threshold_score: number; + passed: boolean; + status: string; + strengths: string[]; + issues: string[]; + improvement_rules: string[]; + quality_gate: unknown; + provider_code: string | null; + model_name: string | null; + error_message: string | null; + created_at: string; + latest_attempt?: { + id: string; + status: string; + error_message: string | null; + created_at: string; + } | null; + } | null; +} + +export interface CharacterTurnaroundMasterResult { + workflow: 'split_panels_then_programmatic_master'; + template_version: string; + master_template_version: string; + character_id: string; + output_size: '2560x1440' | '3840x2160'; + identity_reference: { + assetIds: string[]; + cropMode: 'full' | 'turnaround_face_panel'; + source: string; + }; + panel_reference_map: Record; + panel_images: SafeCharacterImage[]; + master_image: SafeCharacterImage; + quality_gate: { + s_plus_threshold: number; + panel_gate_passed: boolean; + master_gate_passed: boolean; + approved_for_s_plus_video: boolean; + panel_scores: Array<{ image_type: string; score: number | null; passed: boolean }>; + master_score: number | null; + }; + execution_evidence: { + task_ids: string[]; + visual_qc_provider_log_ids: string[]; + source_asset_ids: Array; + master_asset_id: string | null; + image_generation_cost_actual: number; + visual_qc_cost_actual: number; + programmatic_compose_cost_actual: number; + total_cost_actual: number; + }; + next_step: string; +} + +export interface SafeProjectVisualAsset { + id: string; + project_id: string; + user_id: string; + asset_id: string | null; + asset_kind: 'character' | 'prop' | 'scene' | string; + asset_type: string; + name: string; + label: string | null; + ownership_type: + | 'role_identity' + | 'role_exclusive' + | 'shared_story_asset' + | 'neutral_asset' + | 'scene_lock' + | 'exclusive_character' + | 'shared_story' + | 'neutral' + | string; + owner_character_id: string | null; + source_type: string; + source_label: string | null; + aliases?: string[]; + allowed_roles?: string[]; + detected_from: string | null; + importance: number; + visual_lock: string | null; + key_objects?: string[]; + prompt_block: string | null; + anchor_prompt: string | null; + prompt_text: string | null; + negative_prompt: string | null; + reference_images?: string[]; + anchor_images?: string[]; + render_variants_json?: unknown; + linked_assets?: string[]; + reuse_rule: string | null; + story_scope: string | null; + version_note: string | null; + notes: string | null; + usage_tags: string[]; + tags?: string[]; + quality_score: number | null; + is_primary: boolean; + metadata_json: unknown; + status: string; + created_at: string; + updated_at: string; + asset?: SafeAsset | null; + owner_character?: Pick | null; +} + +export interface ProjectIpAssetConflict { + code: string; + severity: 'warning' | 'error'; + asset_ids: string[]; + message: string; + suggestion: string; +} + +export interface ProjectIpAssetConflictReport { + project_id: string; + conflicts: ProjectIpAssetConflict[]; + summary: { + total_assets: number; + conflict_count: number; + error_count: number; + warning_count: number; + }; } export interface SafeEpisode { id: string; project_id: string; episode_no: number; + source_chapter_ids: unknown; title: string | null; summary: string | null; opening_hook: string | null; @@ -218,18 +967,83 @@ export interface SafeEpisode { updated_at: string; } +export interface EpisodePlanPromptOverrides { + overview?: string; + batch?: string; + reconcile?: string; +} + +export interface EpisodePlanRequestParamsOverride { + max_output_tokens?: number; + temperature?: number; + timeout_ms?: number; +} + +export interface EpisodePlanRequestPreviewStage { + key: string; + pipeline_stage: string; + title: string; + prompt: string; + input_json: Record; +} + +export interface EpisodePlanRequestPreview { + project_id: string; + provider_code: string; + episode_count_mode?: 'fixed' | 'ai_recommend'; + reference_episode_count?: number; + recommended_episode_count?: number; + episode_count_reasoning?: string; + episode_count_duration_policy?: string; + episode_count_split_merge_rules?: string[]; + target_episode_count: number; + min_quality_score: number; + mode: string; + batch_size: number; + total_batches: number; + quality_duration_range: { + min: number; + max: number; + }; + episode_duration_reference: number; + request_params_override: EpisodePlanRequestParamsOverride; + prompt_overrides: EpisodePlanPromptOverrides; + stages: EpisodePlanRequestPreviewStage[]; +} + export interface SafeEpisodeScript { id: string; project_id: string; episode_id: string; script_text: string | null; narration_text: string | null; + dialogue_json: unknown; version: number; status: string; created_at: string; updated_at: string; } +export interface ScriptRequestParamsOverride { + max_output_tokens?: number; + temperature?: number; + timeout_ms?: number; +} + +export interface ScriptRequestPreview { + episode_id: string; + project_id: string; + provider_code: string; + target_duration: number; + prompt: string; + input_json: Record; + fallback_draft: { + script_text?: string; + narration_text?: string; + dialogue_json?: unknown; + }; +} + export interface SafeStoryboardShot { id: string; project_id: string; @@ -244,13 +1058,85 @@ export interface SafeStoryboardShot { camera_motion: string | null; effect_type: string | null; duration: number | null; + scene_type: string | null; + importance_score: number | null; + emotion_score: number | null; + action_score: number | null; + route_tier: string | null; prompt_text: string | null; negative_prompt: string | null; + live_action_desc: string | null; + actor_action: string | null; + camera_instruction: string | null; + performance_instruction: string | null; + video_prompt: string | null; + transition_to_next: string | null; + transition_to_next_duration: number | null; + transition_to_next_reason: string | null; + keyframe_asset_id: string | null; + video_clip_asset_id: string | null; + video_status: string | null; status: string; created_at: string; updated_at: string; } +export interface SafeStoryboardVisualBoardCell { + shot_id: string; + shot_no: number; + board_no: number; + row_no: number; + cell_no: number; + global_cell_no: number; + scene_name: string | null; + shot_type: string | null; + visual_desc: string | null; + action_desc: string | null; + dialogue_text: string | null; + duration: number | null; + required_assets_text: string; +} + +export interface SafeStoryboardVisualBoardGroup { + group_no: number; + board_no: number; + row_no: number; + cell_range: string; + shot_ids: string[]; + shot_nos: number[]; + suggested_duration: number; + prompt_text: string; + negative_prompt: string; + cells: SafeStoryboardVisualBoardCell[]; +} + +export interface SafeStoryboardVisualBoard { + board_no: number; + grid_mode: 25; + title: string; + prompt_text: string; + negative_prompt: string; + cells: SafeStoryboardVisualBoardCell[]; +} + +export interface SafeStoryboardVisualBoardPlan { + mode: 'visual_board'; + grid_mode: 25; + group_size: number; + board_count: number; + total_cells: number; + provider_code: string | null; + provider_label: string; + provider_duration_range: { + min: number; + max: number; + allowed: number[]; + }; + boards: SafeStoryboardVisualBoard[]; + groups: SafeStoryboardVisualBoardGroup[]; + copy_package_text: string; +} + export interface SafeCreativePattern { id: string; source_case_id: string | null; @@ -305,6 +1191,26 @@ export interface SafeRenderTask { finished_at: string | null; } +export interface SafeCharacterAnchorVideoTest { + character_id: string; + anchor_asset_id: string | null; + purpose?: 'character_anchor_video_test' | 'video_character_element_source' | string; + status: 'missing_anchor' | 'untested' | 'running' | 'passed' | 'failed' | string; + label: string; + progress: number; + task: SafeRenderTask | null; + output_asset: SafeAsset | null; + output_asset_id: string | null; + historical_passed_anchor_asset_id?: string | null; + historical_passed_output_asset_id?: string | null; + historical_passed_output_asset?: SafeAsset | null; + cost_actual: number | null; + historical_cost_actual?: number | null; + provider_request_id: string | null; + error_message: string | null; + tested_at: string | null; +} + export interface BillingPackage { code: string; name: string; @@ -397,6 +1303,16 @@ export interface SafePlotMemory { created_at: string; } +export interface SafeCharacterMemory { + id: string; + project_id: string; + character_id: string; + episode_id: string | null; + memory_type: string; + content: string; + created_at: string; +} + export interface SafePlotThread { id: string; project_id: string; @@ -509,12 +1425,18 @@ export interface SafeLiveActionShot { actor_action: string | null; camera_instruction: string | null; performance_instruction: string | null; + transition_to_next: string | null; + transition_to_next_duration: string | null; + transition_to_next_reason: string | null; scene_type: string | null; importance_score: number | null; emotion_score: number | null; action_score: number | null; route_tier: string | null; + generation_strategy_mode: 'text_only' | 'first_frame' | 'first_last_frame' | 'multi_reference' | null; + scene_geography_version_id: string | null; video_prompt: string | null; + active_generation_plan_id: string | null; keyframe_asset_id: string | null; video_clip_asset_id: string | null; video_status: string | null; @@ -528,21 +1450,95 @@ export interface SafeVideoClip { project_id: string; episode_id: string; shot_id: string; + generation_plan_id: string | null; provider_id: string | null; + provider_code: string | null; + provider_name: string | null; + model_name: string | null; input_asset_id: string | null; output_asset_id: string | null; duration: string | null; prompt_text: string | null; status: string; + cost_estimate: number | null; + cost_currency: string | null; cost_actual: number | null; retry_count: number; quality_status: string | null; quality_score: number | null; quality_issues: unknown; + engine_version: string | null; + asset_lock_json: unknown; + motion_control_json: unknown; + camera_control_json: unknown; + composer_usage_json: unknown; created_at: string; updated_at: string; } +export interface SafeShotGenerationPlan { + id: string; + project_id: string; + episode_id: string; + shot_id: string; + shot_no: number | null; + scene_name: string | null; + revision: number; + plan_version: string; + plan_hash: string; + status: string; + source_engine_version: string; + provider_code: string; + provider_id: string | null; + capability_registry_version_id: string | null; + parameter_schema_version_id: string | null; + pricing_version_id: string | null; + model_name: string | null; + endpoint: string | null; + capability_version: string | null; + route_tier: string | null; + effective_mode: string | null; + resolution_recommendation: string | null; + effective_generation_resolution: string | null; + aspect_ratio: string | null; + duration: string | null; + sound_enabled: boolean; + multi_shot: boolean; + native_4k_candidate: boolean; + project_native_4k_enabled: boolean; + required_assets_json: unknown; + element_plan_json: unknown; + voice_plan_json: unknown; + keyframe_plan_json: unknown; + video_request_json: unknown; + router_decision_json: unknown; + quality_policy_json: unknown; + retry_policy_json: unknown; + fallback_chain_json: unknown; + cost_policy_json: unknown; + provider_snapshot_json: unknown; + prompt_snapshot_json: unknown; + frozen_by_user_id: string | null; + frozen_at: string; + created_at: string; + is_active: boolean; +} + +export interface GenerationPlanCompareChange { + path: string; + category: string; + before: unknown; + after: unknown; +} + +export interface GenerationPlanCompareReport { + shot_id: string; + base: SafeShotGenerationPlan; + target: SafeShotGenerationPlan; + changed_count: number; + changes: GenerationPlanCompareChange[]; +} + export type LiveActionRepairAction = 'passed' | 'needs_retry' | 'retry_same_provider' | 'switch_provider' | 'manual_required'; export interface LiveActionQualityCheckResult { @@ -560,6 +1556,7 @@ export interface LiveActionQualityCheckResult { } export interface SafeLiveActionVideoProvider { + id: string; provider_code: string; display_name: string | null; mode: string; @@ -572,6 +1569,97 @@ export interface SafeLiveActionVideoProvider { daily_cost_limit: number; } +export interface SafeProviderCatalogItem { + id: string; + provider_type: string; + provider_code: string; + display_name: string | null; + mode: string; + model_name: string | null; + priority: number; + cost_summary?: { + unit: string | null; + currency: string | null; + pricing_basis: string; + estimated_cost_10s: number | null; + estimated_cost_10s_min: number | null; + estimated_cost_10s_max: number | null; + estimated_cost_10s_label: string; + price_per_second: number | null; + price_per_clip: number | null; + max_cost_per_call: number | null; + daily_cost_limit: number | null; + needs_manual_pricing: boolean; + note: string | null; + }; +} + +export interface ProviderLabParameterSchemaItem { + key: string; + label: string; + type: 'text' | 'number' | 'select' | 'boolean' | 'json'; + default_value: unknown; + options: string[]; + help: string; +} + +export interface SafeProviderLabProvider extends SafeProviderCatalogItem { + config_json?: unknown; + cost_summary?: { + unit: string | null; + currency: string | null; + pricing_basis: string; + estimated_cost_10s: number | null; + estimated_cost_10s_min: number | null; + estimated_cost_10s_max: number | null; + estimated_cost_10s_label: string; + price_per_second: number | null; + price_per_clip: number | null; + max_cost_per_call: number | null; + daily_cost_limit: number | null; + needs_manual_pricing: boolean; + note: string | null; + }; + parameter_schema: ProviderLabParameterSchemaItem[]; + upload_hint: string; + max_upload_images?: number; + accepted_image_mime_types?: string[]; + max_upload_size_mb?: number; +} + +export interface ProviderLabOptions { + provider_types: Array<{ key: 'TextProvider' | 'ImageProvider' | 'VideoProvider' | 'MusicProvider' | 'VoiceProvider'; label: string; upload_images: boolean }>; + providers: SafeProviderLabProvider[]; + defaults: Record; +} + +export interface ProviderLabRun { + task: SafeRenderTask; + progress: number; + request_preview: unknown; + uploaded_images: unknown[]; + provider_log: unknown; + result: unknown; + output_asset: SafeAsset | null; + output_preview_url: string | null; + output_text: string | null; +} + +export interface UserModelPreference { + id: string; + user_id: string; + scope_key: string; + project_id: string | null; + preference_key: string; + text_provider_code: string | null; + image_provider_code: string | null; + video_provider_code: string | null; + voice_provider_code: string | null; + metadata_json: unknown; + created_at: string; + updated_at: string; +} + export interface LiveActionCostEstimate { provider_code: string; provider_name: string | null; @@ -606,6 +1694,8 @@ export interface LiveActionPreflightReport { requested_provider_code: string | null; manual_override_allowed: boolean; confirm_real_video: boolean; + allow_seedance_real_person_test: boolean; + allow_character_reference_fallback: boolean; requires_real_video_confirmation: boolean; max_cost_per_clip: number | null; summary: { @@ -632,16 +1722,159 @@ export interface LiveActionPreflightReport { route_tier: string | null; decision_reason: string; estimated_cost: number; + currency: string; + generation_strategy_mode: 'text_only' | 'first_frame' | 'first_last_frame' | 'multi_reference'; + generation_strategy_source: 'legacy' | 'shot_execution_spec' | 'frozen_generation_plan'; + scene_geography_contract_id: string | null; + scene_geography_status: string | null; + scene_geography_ready: boolean; + start_frame_required: boolean; + end_frame_required: boolean; + start_frame_asset_id: string | null; + end_frame_asset_id: string | null; + start_frame_approval_status: string | null; + end_frame_approval_status: string | null; + start_frame_ready: boolean; + end_frame_ready: boolean; + reference_asset_ids: string[]; + reference_assets_ready: boolean; keyframe_asset_id: string | null; keyframe_mime_type: string | null; keyframe_status: string | null; + keyframe_approval_status: string | null; keyframe_ready: boolean; source_image_required: boolean; source_image_ready: boolean; + character_reference_fallback_allowed: boolean; issues: LiveActionPreflightIssue[]; }>; } +export interface LiveActionShotAssetItem { + asset_id: string; + role: string; + asset_type: string; + character_id: string | null; + character_name: string | null; + label: string; + source: 'shot' | 'project_character' | 'global_character' | 'project_asset' | string; + priority?: number; + reason?: string; + tags?: { + emotion: string[]; + wardrobe: string[]; + scene: string[]; + prop?: string[]; + }; + quality_score?: number | null; + consistency_score?: number | null; + is_primary?: boolean; + selector_score?: number; + score_breakdown?: Record; +} + +export interface LiveActionShotAssetRequirement { + role: string; + asset_type: string; + character_id: string | null; + character_name: string | null; + label: string; + required: boolean; + priority: number; +} + +export interface LiveActionShotAssetMissing { + role: string; + character_id: string | null; + character_name: string | null; + label: string; + severity: 'blocker' | 'warning'; +} + +export interface LiveActionShotAssetAlternativeGroup { + role: string; + character_id: string | null; + character_name: string | null; + label: string; + assets: LiveActionShotAssetItem[]; +} + +export interface LiveActionShotAssetPlan { + shot_id: string; + shot_no: number; + scene_name: string | null; + shot_type: string; + recipe_code: string; + recipe_label: string; + recipe_note: string; + max_image_inputs: number; + web_max_image_inputs: number; + provider_payload_profile?: { + provider_code: string; + provider_name: string | null; + provider_mode: string; + model_name: string | null; + payload_mode: string; + supports_reference_image: boolean; + supports_multi_character: boolean; + supports_style_reference: boolean; + max_image_inputs: number; + web_max_image_inputs: number; + max_prompt_length: number; + image_weight_limit: number; + note: string; + }; + selector_context?: { + text: string; + emotion_tags: string[]; + wardrobe_tags: string[]; + scene_tags: string[]; + prop_tags?: string[]; + }; + required_asset_types?: string[]; + selected_asset_ids?: string[]; + web_asset_ids?: string[]; + required_assets: LiveActionShotAssetRequirement[]; + selected_assets: LiveActionShotAssetItem[]; + web_assets: LiveActionShotAssetItem[]; + alternative_assets: LiveActionShotAssetAlternativeGroup[]; + selectable_alternatives?: string[][]; + missing_requirements: LiveActionShotAssetMissing[]; + payload_warnings?: string[]; + degradation_steps?: string[]; + ready: boolean; + next_step: string; + api_payload: Record; + web_payload?: Record; + copy_text: string; +} + +export interface LiveActionShotAssetPlanReport { + episode_id: string; + project_id: string; + provider_capability: { + provider_code: string; + provider_name: string | null; + provider_mode: string; + model_name: string | null; + payload_mode: string; + supports_reference_image: boolean; + supports_multi_character: boolean; + supports_style_reference: boolean; + max_image_inputs: number; + web_max_image_inputs: number; + max_prompt_length: number; + image_weight_limit: number; + note: string; + }; + summary: { + shot_count: number; + ready_count: number; + missing_count: number; + }; + plans: LiveActionShotAssetPlan[]; +} + export interface SafeMediaTaskResult { asset: SafeAsset; task: SafeRenderTask; @@ -803,10 +2036,130 @@ export class UserApiClient { return this.request('/projects'); } + getLibrary() { + return this.request('/projects/library'); + } + + listProjectAssets(projectId: string, query: Record = {}) { + const params = new URLSearchParams(); + + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null || value === '') continue; + params.set(key, String(value)); + } + + const suffix = params.toString() ? `?${params.toString()}` : ''; + return this.request(`/projects/${projectId}/assets${suffix}`); + } + + updateAssetReviewState( + assetId: string, + payload: { + display_name?: string | null; + selection_status?: 'candidate' | 'selected' | 'rejected'; + selection_note?: string | null; + metadata_json?: unknown; + } + ) { + return this.request<{ asset: SafeAsset; next_step: string }>( + `/assets/${assetId}/review-state`, + { method: 'PATCH', body: payload } + ); + } + + getLibraryNovel(sourceId: string) { + return this.request(`/projects/library/novels/${sourceId}`); + } + + listLibraryNovelChapters(sourceId: string, page = 1, limit = 30) { + return this.request( + `/projects/library/novels/${sourceId}/chapters?page=${page}&limit=${limit}` + ); + } + + getLibraryNovelChapter(sourceId: string, chapterId: string) { + return this.request(`/projects/library/novels/${sourceId}/chapters/${chapterId}`); + } + + searchLibraryNovel(sourceId: string, query: string, page = 1, limit = 20) { + const params = new URLSearchParams({ q: query, page: String(page), limit: String(limit) }); + + return this.request(`/projects/library/novels/${sourceId}/search?${params.toString()}`); + } + + getLibraryNovelProgress(sourceId: string) { + return this.request<{ progress: NovelReadingProgress | null }>(`/projects/library/novels/${sourceId}/progress`); + } + + updateLibraryNovelProgress(sourceId: string, payload: { chapter_id: string; page_index: number; page_count: number }) { + return this.request<{ progress: NovelReadingProgress }>(`/projects/library/novels/${sourceId}/progress`, { + method: 'PATCH', + body: payload + }); + } + + listLibraryNovelBookmarks(sourceId: string) { + return this.request<{ bookmarks: NovelBookmark[] }>(`/projects/library/novels/${sourceId}/bookmarks`); + } + + createLibraryNovelBookmark(sourceId: string, payload: { chapter_id: string; page_index: number; title?: string; note_text?: string }) { + return this.request<{ bookmark: NovelBookmark }>(`/projects/library/novels/${sourceId}/bookmarks`, { + method: 'POST', + body: payload + }); + } + + deleteLibraryNovelBookmark(sourceId: string, bookmarkId: string) { + return this.request<{ deleted: boolean }>(`/projects/library/novels/${sourceId}/bookmarks/${bookmarkId}`, { + method: 'DELETE' + }); + } + + listLibraryNovelAnnotations(sourceId: string) { + return this.request<{ annotations: NovelAnnotation[] }>(`/projects/library/novels/${sourceId}/annotations`); + } + + createLibraryNovelAnnotation( + sourceId: string, + payload: { chapter_id: string; page_index: number; selected_text: string; note_text?: string; color?: string } + ) { + return this.request<{ annotation: NovelAnnotation }>(`/projects/library/novels/${sourceId}/annotations`, { + method: 'POST', + body: payload + }); + } + + updateLibraryNovelAnnotation(sourceId: string, annotationId: string, payload: { note_text?: string; color?: string }) { + return this.request<{ annotation: NovelAnnotation }>(`/projects/library/novels/${sourceId}/annotations/${annotationId}`, { + method: 'PATCH', + body: payload + }); + } + + deleteLibraryNovelAnnotation(sourceId: string, annotationId: string) { + return this.request<{ deleted: boolean }>(`/projects/library/novels/${sourceId}/annotations/${annotationId}`, { + method: 'DELETE' + }); + } + createProject(payload: Record) { return this.request('/projects', { method: 'POST', body: payload }); } + createPromptStoryboard(payload: Record) { + return this.request('/projects/prompt-storyboard', { + method: 'POST', + body: payload + }); + } + + previewPromptStoryboard(payload: Record) { + return this.request('/projects/prompt-storyboard/preview', { + method: 'POST', + body: payload + }); + } + listCreativePatternLibrary(params: Record = {}) { const query = new URLSearchParams({ limit: '30', ...params }); @@ -819,10 +2172,91 @@ export class UserApiClient { return this.request(`/projects/${projectId}`); } + getProjectPipelineConfig(projectId: string) { + return this.request<{ config: SafeProjectPipelineConfig }>(`/projects/${projectId}/pipeline-config`); + } + + updateProjectPipelineConfig(projectId: string, payload: { scene_composer_enabled?: boolean; original_music_enabled?: boolean }) { + return this.request<{ config: SafeProjectPipelineConfig }>(`/projects/${projectId}/pipeline-config`, { + method: 'PATCH', + body: payload + }); + } + listProjectCreativePatterns(projectId: string) { return this.request<{ patterns: SafeProjectCreativePattern[] }>(`/projects/${projectId}/creative-patterns`); } + listProviderCatalog(providerType?: string) { + const query = providerType ? `?provider_type=${encodeURIComponent(providerType)}` : ''; + + return this.request(`/providers/catalog${query}`); + } + + listProviderLabOptions(providerType?: string) { + const query = providerType ? `?provider_type=${encodeURIComponent(providerType)}` : ''; + + return this.request(`/provider-lab/options${query}`); + } + + createProviderLabRun(payload: { + project_id?: string | null; + provider_type: 'TextProvider' | 'ImageProvider' | 'VideoProvider' | 'MusicProvider' | 'VoiceProvider'; + provider_code: string; + prompt: string; + params: Record; + images: File[]; + confirm_paid_test: boolean; + }) { + const form = new FormData(); + + if (payload.project_id) form.set('project_id', payload.project_id); + form.set('provider_type', payload.provider_type); + form.set('provider_code', payload.provider_code); + form.set('prompt', payload.prompt); + form.set('params_json', JSON.stringify(payload.params ?? {})); + form.set('confirm_paid_test', payload.confirm_paid_test ? 'true' : 'false'); + for (const file of payload.images ?? []) { + form.append('images', file, file.name); + } + + return this.request('/provider-lab/runs', { + method: 'POST', + body: form + }); + } + + getProviderLabRun(taskId: string) { + return this.request(`/provider-lab/runs/${taskId}`); + } + + listModelPreferences(options: { project_id?: string; scope_key?: string } = {}) { + const query = new URLSearchParams(); + if (options.project_id) query.set('project_id', options.project_id); + if (options.scope_key) query.set('scope_key', options.scope_key); + const suffix = query.toString() ? `?${query.toString()}` : ''; + + return this.request<{ preferences: UserModelPreference[] }>(`/me/model-preferences${suffix}`); + } + + saveModelPreferences(payload: { + project_id?: string | null; + scope_key?: string; + preferences: Array<{ + preference_key: string; + text_provider_code?: string | null; + image_provider_code?: string | null; + video_provider_code?: string | null; + voice_provider_code?: string | null; + metadata_json?: unknown; + }>; + }) { + return this.request<{ preferences: UserModelPreference[]; scope_key: string }>('/me/model-preferences', { + method: 'POST', + body: payload + }); + } + updateProjectCreativePatterns(projectId: string, creativePatternIds: string[]) { return this.request<{ patterns: SafeProjectCreativePattern[] }>(`/projects/${projectId}/creative-patterns`, { method: 'PATCH', @@ -933,19 +2367,157 @@ export class UserApiClient { }>(`/projects/${projectId}/original/result`); } - generateStoryBible(projectId: string) { - return this.request<{ story_bible: SafeStoryBible; next_step: string }>( - `/projects/${projectId}/story-bible/generate`, + createProductionSourceSnapshot(projectId: string, payload: { novel_source_id?: string; chapter_ids?: string[] } = {}) { + return this.request<{ source_snapshot: SafeProductionSourceSnapshot; reused: boolean }>( + `/projects/${projectId}/production/source-snapshots`, + { method: 'POST', body: payload } + ); + } + + listProductionSourceSnapshots(projectId: string) { + return this.request<{ source_snapshots: SafeProductionSourceSnapshot[] }>( + `/projects/${projectId}/production/source-snapshots` + ); + } + + listProductionContracts(projectId: string, contractType?: string) { + const query = contractType ? `?contract_type=${encodeURIComponent(contractType)}` : ''; + return this.request<{ contracts: SafeProductionContract[] }>( + `/projects/${projectId}/production/contracts${query}` + ); + } + + getProductionContract(contractId: string) { + return this.request<{ contract: SafeProductionContract; reviews: SafeProductionContractReview[] }>( + `/production/contracts/${contractId}` + ); + } + + saveProductionContract( + projectId: string, + contractType: SafeProductionContract['contract_type'], + payload: { payload: Record; parent_contract_id?: string; source_snapshot_id?: string } + ) { + return this.request<{ contract: SafeProductionContract; quality: ProductionQualityResult }>( + `/projects/${projectId}/production/contracts/${contractType}`, + { method: 'POST', body: payload } + ); + } + + previewProductionStagePrompt( + projectId: string, + stage: ProductionStagePromptPreview['stage'], + payload: { source_snapshot_id?: string; upstream_contract_id?: string; episode_number?: number } + ) { + return this.request( + `/projects/${projectId}/production/prompts/${stage}/preview`, + { method: 'POST', body: payload } + ); + } + + reviewProductionContract(contractId: string, reviewerNotes: string[] = []) { + return this.request<{ review: Record; quality: ProductionQualityResult; delta: unknown; reviewer_prompt: string }>( + `/production/contracts/${contractId}/review`, + { method: 'POST', body: { reviewer_notes: reviewerNotes } } + ); + } + + aiReviewProductionSceneScript( + contractId: string, + payload: { provider_code?: string; reviewer_notes?: string[]; request_params_override?: Record } = {} + ) { + return this.request<{ + review: SafeProductionContractReview; + report: SplusSceneScriptReviewReport; + deterministic_quality: ProductionQualityResult; + provider: Record; + provider_log: Record; + }>(`/production/contracts/${contractId}/ai-review`, { method: 'POST', body: payload }); + } + + confirmProductionContract(contractId: string) { + return this.request<{ contract: SafeProductionContract; review: Record }>( + `/production/contracts/${contractId}/confirm`, { method: 'POST', body: {} } ); } + checkProductionEpisodeContinuity(projectId: string, contractIds: string[]) { + return this.request<{ passed: boolean; episode_contract_ids: string[]; quality: ProductionQualityResult; delta: unknown }>( + `/projects/${projectId}/production/episode-continuity/check`, + { method: 'POST', body: { contract_ids: contractIds } } + ); + } + + releaseProductionEpisodePlan(contractId: string) { + return this.request<{ contract: SafeProductionContract }>( + `/production/contracts/${contractId}/release-to-script`, + { method: 'POST', body: {} } + ); + } + + materializeProductionExecutionDraft(contractId: string, shotSpecs: Array>) { + return this.request<{ + bridge_version: string; + scene_geography_contract_id: string; + episode_id: string; + episode_script_id: string; + shot_count: number; + total_duration_ms: number; + execution_status: 'external_assets_pending'; + generation_plan_frozen: false; + shots: Array<{ + id: string; + shot_no: number; + scene_name: string | null; + duration: number | null; + video_status: string | null; + status: string; + }>; + }>( + `/production/contracts/${contractId}/materialize-execution-draft`, + { method: 'POST', body: { shot_specs: shotSpecs } } + ); + } + + compileFlowTestGenerationPlans(contractId: string, waiverReason: string) { + return this.request( + `/production/contracts/${contractId}/compile-flow-test-generation-plans`, + { + method: 'POST', + body: { + confirm_flow_test_waiver: true, + waiver_reason: waiverReason + } + } + ); + } + + generateStoryBible(projectId: string, options: { provider_code?: string } = {}) { + return this.request<{ + story_bible: SafeStoryBible | null; + task?: SafeRenderTask | null; + next_step: string; + already_running?: boolean; + }>( + `/projects/${projectId}/story-bible/generate`, + { method: 'POST', body: { provider_code: options.provider_code || undefined } } + ); + } + getStoryBible(projectId: string) { return this.request<{ story_bible: SafeStoryBible | null; versions: Array> }>( `/projects/${projectId}/story-bible` ); } + updateStoryBible(projectId: string, payload: Record) { + return this.request<{ story_bible: SafeStoryBible; previous_version: number; next_step: string }>( + `/projects/${projectId}/story-bible`, + { method: 'PATCH', body: payload } + ); + } + confirmStoryBible(projectId: string) { return this.request<{ story_bible: SafeStoryBible; next_step: string }>( `/projects/${projectId}/story-bible/confirm`, @@ -953,10 +2525,30 @@ export class UserApiClient { ); } - extractCharacters(projectId: string) { + extractCharacters(projectId: string, options: { provider_code?: string } = {}) { return this.request<{ characters: SafeCharacter[]; next_step: string }>( `/projects/${projectId}/characters/extract`, - { method: 'POST', body: {} } + { method: 'POST', body: { provider_code: options.provider_code || undefined } } + ); + } + + listCharacterExtractionVersions(projectId: string) { + return this.request<{ versions: SafeCharacterExtractionVersion[] }>( + `/projects/${projectId}/characters/extraction-versions` + ); + } + + importCharacterExtraction(projectId: string, payload: Record) { + return this.request<{ version: SafeCharacterExtractionVersion; next_step: string }>( + `/projects/${projectId}/characters/extraction-versions/import`, + { method: 'POST', body: payload } + ); + } + + updateCharacterExtractionReview(projectId: string, versionId: string, payload: Record) { + return this.request<{ version: SafeCharacterExtractionVersion }>( + `/projects/${projectId}/characters/extraction-versions/${versionId}/review`, + { method: 'PATCH', body: payload } ); } @@ -964,20 +2556,158 @@ export class UserApiClient { return this.request(`/projects/${projectId}/characters`); } + listCharacterAnchorVideoTests(projectId: string) { + return this.request<{ tests: SafeCharacterAnchorVideoTest[] }>( + `/projects/${projectId}/characters/anchor-video-tests` + ); + } + + getCharacterAnchorVideoTest(characterId: string) { + return this.request<{ test: SafeCharacterAnchorVideoTest }>( + `/characters/${characterId}/anchor-video-test` + ); + } + + createCharacterAnchorVideoTest( + characterId: string, + payload: { + provider_code?: string; + duration?: number; + force?: boolean; + purpose?: 'anchor_test' | 'video_character_element_source'; + reference_mode?: 'first_frame' | 'omni_reference'; + voice_line?: string; + generate_audio?: boolean; + source_asset_id?: string; + prompt_override?: string; + negative_prompt?: string; + resolution?: '720p' | '1080p' | '4k'; + mode?: 'std' | 'pro' | '4k'; + } = {} + ) { + return this.request<{ test: SafeCharacterAnchorVideoTest; next_step: string }>( + `/characters/${characterId}/anchor-video-test`, + { + method: 'POST', + body: payload + } + ); + } + + listProjectVisualAssets(projectId: string, assetKind?: 'prop' | 'scene' | string) { + const query = assetKind ? `?asset_kind=${encodeURIComponent(assetKind)}` : ''; + + return this.request<{ assets: SafeProjectVisualAsset[] }>( + `/projects/${projectId}/visual-assets${query}` + ); + } + + listProjectIpAssets(projectId: string, assetKind?: 'character' | 'prop' | 'scene' | string) { + const query = assetKind ? `?asset_kind=${encodeURIComponent(assetKind)}` : ''; + + return this.request<{ assets: SafeProjectVisualAsset[]; ip_assets: SafeProjectVisualAsset[] }>( + `/projects/${projectId}/ops/ip-assets${query}` + ); + } + + listProjectIpAssetConflicts(projectId: string) { + return this.request(`/projects/${projectId}/ops/ip-assets/conflicts`); + } + + extractProjectIpAssets(projectId: string, payload: Record = {}) { + return this.request<{ + assets: SafeProjectVisualAsset[]; + ip_assets: SafeProjectVisualAsset[]; + created_count: number; + updated_count: number; + skipped_count: number; + next_step: string; + }>( + `/projects/${projectId}/ops/ip-assets/extract`, + { method: 'POST', body: payload } + ); + } + + optimizeProjectIpAssetPrompts(projectId: string, payload: Record = {}) { + return this.request<{ + assets: SafeProjectVisualAsset[]; + ip_assets: SafeProjectVisualAsset[]; + optimized_count: number; + skipped_count: number; + min_quality_score: number; + next_step: string; + }>( + `/projects/${projectId}/ops/ip-assets/optimize-prompts`, + { method: 'POST', body: payload } + ); + } + + createProjectVisualAsset(projectId: string, payload: Record) { + return this.request<{ asset: SafeProjectVisualAsset; next_step: string }>( + `/projects/${projectId}/visual-assets`, + { method: 'POST', body: payload } + ); + } + + createProjectIpAsset(projectId: string, payload: Record) { + return this.request<{ asset: SafeProjectVisualAsset; ip_asset: SafeProjectVisualAsset; next_step: string }>( + `/projects/${projectId}/ops/ip-assets`, + { method: 'POST', body: payload } + ); + } + + updateProjectVisualAsset(projectId: string, visualAssetId: string, payload: Record) { + return this.request<{ asset: SafeProjectVisualAsset; next_step: string }>( + `/projects/${projectId}/visual-assets/${visualAssetId}`, + { method: 'PATCH', body: payload } + ); + } + + deleteProjectIpAsset(projectId: string, visualAssetId: string) { + return this.request<{ deleted: boolean; asset_id: string; next_step: string }>( + `/projects/${projectId}/ops/ip-assets/${visualAssetId}`, + { method: 'DELETE' } + ); + } + confirmCharacters(projectId: string) { - return this.request<{ characters: SafeCharacter[]; next_step: string }>( + return this.request<{ characters: SafeCharacter[]; voice_profiles?: unknown[]; next_step: string }>( `/projects/${projectId}/characters/confirm`, { method: 'POST', body: {} } ); } - generatePlotMemories(projectId: string) { + autoBindCharacterVoices( + projectId: string, + payload: { voice_provider_code?: string | null; force?: boolean } = {} + ) { + return this.request<{ characters: SafeCharacter[]; voice_profiles: unknown[]; next_step: string }>( + `/projects/${projectId}/characters/voice-profiles/auto-bind`, + { method: 'POST', body: payload } + ); + } + + generatePlotMemories( + projectId: string, + options: { provider_code?: string; replace_existing?: boolean; min_quality_score?: number } = {} + ) { return this.request<{ + task?: SafeRenderTask; plot_memories: SafePlotMemory[]; + character_memories?: SafeCharacterMemory[]; plot_threads: SafePlotThread[]; created_count: Record; + quality_report?: unknown; next_step: string; - }>(`/projects/${projectId}/plot-memories/generate`, { method: 'POST', body: {} }); + already_running?: boolean; + }>(`/projects/${projectId}/plot-memories/generate`, { + method: 'POST', + body: { + provider_code: options.provider_code || undefined, + replace_existing: options.replace_existing === true, + min_quality_score: options.min_quality_score + } + }); } listPlotMemories(projectId: string) { @@ -988,6 +2718,10 @@ export class UserApiClient { return this.request(`/projects/${projectId}/plot-threads`); } + listProjectCharacterMemories(projectId: string) { + return this.request(`/projects/${projectId}/character-memories`); + } + updateCharacter(characterId: string, payload: Record) { return this.request(`/characters/${characterId}`, { method: 'PATCH', @@ -995,6 +2729,246 @@ export class UserApiClient { }); } + listMyGlobalCharacters() { + return this.request<{ characters: MyGlobalCharacterRow[] }>('/me/global-characters'); + } + + createMyGlobalCharacter(payload: Record) { + return this.request<{ + character: SafeGlobalCharacter; + assets: SafeGlobalCharacterAsset[]; + next_step: string; + }>('/me/global-characters', { + method: 'POST', + body: payload + }); + } + + updateMyGlobalCharacter(globalCharacterId: string, payload: Record) { + return this.request<{ + character: SafeGlobalCharacter; + assets: SafeGlobalCharacterAsset[]; + }>(`/me/global-characters/${globalCharacterId}`, { + method: 'PATCH', + body: payload + }); + } + + listCharacterProviderBindings(globalCharacterId: string) { + return this.request<{ + character: SafeGlobalCharacter; + provider_bindings: SafeCharacterProviderBinding[]; + api_binding_rule: string; + }>(`/me/global-characters/${globalCharacterId}/provider-bindings`); + } + + importCharacterProviderBinding(globalCharacterId: string, payload: Record) { + return this.request<{ + binding: SafeCharacterProviderBinding; + provider_bindings: SafeCharacterProviderBinding[]; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/provider-bindings`, { + method: 'POST', + body: payload + }); + } + + updateCharacterProviderBinding(globalCharacterId: string, bindingId: string, payload: Record) { + return this.request<{ + binding: SafeCharacterProviderBinding; + provider_bindings: SafeCharacterProviderBinding[]; + }>(`/me/global-characters/${globalCharacterId}/provider-bindings/${bindingId}`, { + method: 'PATCH', + body: payload + }); + } + + setMyGlobalCharacterAssets( + globalCharacterId: string, + payload: { + asset_ids: string[]; + look_version_id?: string; + consent_confirmed: boolean; + set_primary_anchor?: boolean; + label?: string; + notes?: string; + } + ) { + return this.request<{ + character: SafeGlobalCharacter; + assets: SafeGlobalCharacterAsset[]; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/assets`, { + method: 'POST', + body: payload + }); + } + + createGlobalCharacterLookVersion(globalCharacterId: string, payload: Record) { + return this.request<{ + character: SafeGlobalCharacter; + look_version: SafeGlobalCharacterLookVersion; + look_versions: SafeGlobalCharacterLookVersion[]; + assets: SafeGlobalCharacterAsset[]; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/look-versions`, { + method: 'POST', + body: payload + }); + } + + importGlobalCharacterAsset(globalCharacterId: string, payload: Record) { + return this.request<{ + character: SafeGlobalCharacter; + asset: SafeGlobalCharacterAsset; + look_versions: SafeGlobalCharacterLookVersion[]; + assets: SafeGlobalCharacterAsset[]; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/assets/import`, { + method: 'POST', + body: payload + }); + } + + setGlobalCharacterPrimaryAsset(globalCharacterId: string, payload: Record) { + return this.request<{ + character: SafeGlobalCharacter; + look_versions: SafeGlobalCharacterLookVersion[]; + assets: SafeGlobalCharacterAsset[]; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/assets/set-primary`, { + method: 'POST', + body: payload + }); + } + + getGlobalCharacterVisualAnchorCard(globalCharacterId: string, lookVersionId?: string | null) { + const query = lookVersionId ? `?look_version_id=${encodeURIComponent(lookVersionId)}` : ''; + return this.request<{ card: SafeVisualAnchorCard }>( + `/me/global-characters/${globalCharacterId}/visual-anchor-card${query}` + ); + } + + listGlobalCharacterPromptVersions(globalCharacterId: string) { + return this.request<{ prompt_versions: SafeCharacterPromptVersion[] }>( + `/me/global-characters/${globalCharacterId}/prompt-versions` + ); + } + + createGlobalCharacterPromptVersion(globalCharacterId: string, payload: Record) { + return this.request<{ + prompt_version: SafeCharacterPromptVersion; + prompt_versions: SafeCharacterPromptVersion[]; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/prompt-versions`, { + method: 'POST', + body: payload + }); + } + + activateGlobalCharacterPromptVersion(globalCharacterId: string, versionId: string) { + return this.request<{ + prompt_version: SafeCharacterPromptVersion; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/prompt-versions/${versionId}/activate`, { + method: 'POST' + }); + } + + refreshGlobalCharacterSystemPrompt(globalCharacterId: string) { + return this.request<{ + disabled_count: number; + next_step: string; + }>(`/me/global-characters/${globalCharacterId}/prompt-versions/refresh-system`, { + method: 'POST' + }); + } + + updateGlobalCharacterAssetReview( + globalCharacterId: string, + globalCharacterAssetId: string, + payload: Record + ) { + return this.request<{ + character: SafeGlobalCharacter; + asset: SafeGlobalCharacterAsset; + look_versions: SafeGlobalCharacterLookVersion[]; + assets: SafeGlobalCharacterAsset[]; + }>(`/me/global-characters/${globalCharacterId}/assets/${globalCharacterAssetId}/review`, { + method: 'PATCH', + body: payload + }); + } + + promoteCharacterToGlobal(characterId: string, payload: Record) { + return this.request<{ + character: SafeCharacter; + global_character: SafeGlobalCharacter; + look_version: SafeGlobalCharacterLookVersion; + assets: SafeGlobalCharacterAsset[]; + mode?: 'created_new' | 'synced_existing'; + next_step: string; + }>(`/characters/${characterId}/promote-to-global`, { + method: 'POST', + body: payload + }); + } + + bindCharacterIp(characterId: string, payload: Record) { + return this.request<{ + character: SafeCharacter; + global_character: SafeGlobalCharacter; + look_version: SafeGlobalCharacterLookVersion | null; + global_asset: SafeGlobalCharacterAsset | null; + next_step: string; + }>(`/characters/${characterId}/bind-global-ip`, { + method: 'POST', + body: payload + }); + } + + getCharacterVisualAnchorCard(characterId: string, lookVersionId?: string | null) { + const query = lookVersionId ? `?look_version_id=${encodeURIComponent(lookVersionId)}` : ''; + return this.request<{ card: SafeVisualAnchorCard }>( + `/characters/${characterId}/visual-anchor-card${query}` + ); + } + + listCharacterPromptVersions(characterId: string) { + return this.request<{ prompt_versions: SafeCharacterPromptVersion[] }>( + `/characters/${characterId}/prompt-versions` + ); + } + + createCharacterPromptVersion(characterId: string, payload: Record) { + return this.request<{ + prompt_version: SafeCharacterPromptVersion; + prompt_versions: SafeCharacterPromptVersion[]; + next_step: string; + }>(`/characters/${characterId}/prompt-versions`, { + method: 'POST', + body: payload + }); + } + + activateCharacterPromptVersion(characterId: string, versionId: string) { + return this.request<{ + prompt_version: SafeCharacterPromptVersion; + next_step: string; + }>(`/characters/${characterId}/prompt-versions/${versionId}/activate`, { + method: 'POST' + }); + } + + refreshCharacterSystemPrompt(characterId: string) { + return this.request<{ + disabled_count: number; + next_step: string; + }>(`/characters/${characterId}/prompt-versions/refresh-system`, { + method: 'POST' + }); + } + listCharacterImages(characterId: string) { return this.request(`/characters/${characterId}/images`); } @@ -1006,6 +2980,14 @@ export class UserApiClient { count_per_type?: number; force?: boolean; set_first_as_anchor?: boolean; + use_reference_images?: boolean; + reference_asset_ids?: string[]; + reference_crop_mode?: 'full' | 'turnaround_face_panel'; + character_description_override?: string; + output_size?: '2560x1440' | '3840x2160'; + enable_visual_quality_review?: boolean; + provider_code?: string; + prompt_review_provider_code?: string; } = {} ) { return this.request(`/characters/${characterId}/generate-images`, { @@ -1014,7 +2996,61 @@ export class UserApiClient { image_types: options.image_types ?? ['anchor'], count_per_type: options.count_per_type ?? 1, set_first_as_anchor: options.set_first_as_anchor ?? true, - force: options.force ?? false + use_reference_images: options.use_reference_images ?? true, + reference_asset_ids: options.reference_asset_ids?.length + ? options.reference_asset_ids + : undefined, + reference_crop_mode: options.reference_crop_mode || undefined, + character_description_override: options.character_description_override?.trim() || undefined, + output_size: options.output_size || undefined, + enable_visual_quality_review: options.enable_visual_quality_review ?? true, + force: options.force ?? false, + provider_code: options.provider_code || undefined, + prompt_review_provider_code: options.prompt_review_provider_code || undefined + } + }); + } + + generateCharacterTurnaroundMaster( + characterId: string, + options: { + provider_code?: string; + prompt_review_provider_code?: string; + reference_asset_ids?: string[]; + character_description_override?: string; + output_size?: '2560x1440' | '3840x2160'; + enable_visual_quality_review?: boolean; + force?: boolean; + regenerate_image_types?: string[]; + } = {} + ) { + return this.request( + `/characters/${characterId}/turnaround-master/generate`, + { + method: 'POST', + body: { + provider_code: options.provider_code || undefined, + prompt_review_provider_code: options.prompt_review_provider_code || undefined, + reference_asset_ids: options.reference_asset_ids?.length + ? options.reference_asset_ids + : undefined, + character_description_override: options.character_description_override?.trim() || undefined, + output_size: options.output_size || '3840x2160', + enable_visual_quality_review: options.enable_visual_quality_review ?? true, + force: options.force ?? true, + regenerate_image_types: options.regenerate_image_types?.length + ? options.regenerate_image_types + : undefined + } + } + ); + } + + reviewCharacterImage(characterId: string, imageId: string, providerCode?: string) { + return this.request(`/characters/${characterId}/images/${imageId}/review-visual`, { + method: 'POST', + body: { + provider_code: providerCode || undefined } }); } @@ -1026,10 +3062,111 @@ export class UserApiClient { }); } - generateEpisodes(projectId: string, targetEpisodeCount?: number | null) { - return this.request<{ episodes: SafeEpisode[]; next_step: string }>( + importCharacterImage( + characterId: string, + payload: { + asset_id: string; + image_type?: string; + set_as_anchor?: boolean; + prompt_text?: string; + } + ) { + return this.request<{ + character_id: string; + anchor_asset_id: string | null; + image: SafeCharacterImage; + set_as_anchor: boolean; + next_step: string; + }>(`/characters/${characterId}/import-image`, { + method: 'POST', + body: payload + }); + } + + setCharacterFacePack( + characterId: string, + payload: { + asset_ids: string[]; + consent_confirmed: boolean; + set_first_as_anchor?: boolean; + notes?: string; + } + ) { + return this.request<{ + character_id: string; + anchor_asset_id: string | null; + face_reference_count: number; + actor_profile_id: string; + images: SafeCharacterImage[]; + next_step: string; + }>(`/characters/${characterId}/face-pack`, { + method: 'POST', + body: payload + }); + } + + previewEpisodePlanRequest( + projectId: string, + payload: { + target_episode_count?: number | null; + episode_count_mode?: 'fixed' | 'ai_recommend'; + force?: boolean; + provider_code?: string; + min_quality_score?: number; + prompt_overrides?: EpisodePlanPromptOverrides; + request_params_override?: EpisodePlanRequestParamsOverride; + } = {} + ) { + return this.request( + `/projects/${projectId}/episodes/plan-request-preview`, + { + method: 'POST', + body: { + target_episode_count: payload.target_episode_count ?? undefined, + episode_count_mode: payload.episode_count_mode ?? undefined, + force: payload.force === true ? true : undefined, + provider_code: payload.provider_code || undefined, + min_quality_score: payload.min_quality_score ?? undefined, + prompt_overrides: payload.prompt_overrides, + request_params_override: payload.request_params_override + } + } + ); + } + + generateEpisodes( + projectId: string, + targetEpisodeCount?: number | null, + options: { + episode_count_mode?: 'fixed' | 'ai_recommend'; + force?: boolean; + provider_code?: string; + min_quality_score?: number; + prompt_overrides?: EpisodePlanPromptOverrides; + request_params_override?: EpisodePlanRequestParamsOverride; + } = {} + ) { + return this.request<{ + episodes: SafeEpisode[]; + memory_context?: unknown; + quality_report?: unknown; + task?: SafeRenderTask | null; + already_running?: boolean; + next_step: string; + }>( `/projects/${projectId}/episodes/generate-plan`, - { method: 'POST', body: { target_episode_count: targetEpisodeCount ?? undefined } } + { + method: 'POST', + body: { + target_episode_count: targetEpisodeCount ?? undefined, + episode_count_mode: options.episode_count_mode ?? undefined, + force: options.force === true ? true : undefined, + provider_code: options.provider_code || undefined, + min_quality_score: options.min_quality_score ?? undefined, + prompt_overrides: options.prompt_overrides, + request_params_override: options.request_params_override + } + } ); } @@ -1044,10 +3181,37 @@ export class UserApiClient { ); } - generateScript(episodeId: string) { + previewScriptRequest( + episodeId: string, + options: { provider_code?: string; prompt_override?: string; request_params_override?: ScriptRequestParamsOverride } = {} + ) { + return this.request( + `/episodes/${episodeId}/script/request-preview`, + { + method: 'POST', + body: { + provider_code: options.provider_code || undefined, + prompt_override: options.prompt_override || undefined, + request_params_override: options.request_params_override + } + } + ); + } + + generateScript( + episodeId: string, + options: { provider_code?: string; prompt_override?: string; request_params_override?: ScriptRequestParamsOverride } = {} + ) { return this.request<{ script: SafeEpisodeScript; next_step: string }>( `/episodes/${episodeId}/script/generate`, - { method: 'POST', body: {} } + { + method: 'POST', + body: { + provider_code: options.provider_code || undefined, + prompt_override: options.prompt_override || undefined, + request_params_override: options.request_params_override + } + } ); } @@ -1062,10 +3226,18 @@ export class UserApiClient { ); } - generateStoryboard(episodeId: string) { + generateStoryboard(episodeId: string, options: { provider_code?: string; video_provider_code?: string; storyboard_mode?: 'visual_board' | 'shot_list'; force?: boolean } = {}) { return this.request<{ shots: SafeStoryboardShot[]; next_step: string }>( `/episodes/${episodeId}/storyboard/generate`, - { method: 'POST', body: {} } + { + method: 'POST', + body: { + provider_code: options.provider_code || undefined, + video_provider_code: options.video_provider_code || undefined, + storyboard_mode: options.storyboard_mode || undefined, + force: options.force === true ? true : undefined + } + } ); } @@ -1073,6 +3245,13 @@ export class UserApiClient { return this.request(`/episodes/${episodeId}/storyboard`); } + getStoryboardVisualBoardPlan(episodeId: string, videoProviderCode?: string) { + const search = new URLSearchParams(); + if (videoProviderCode) search.set('video_provider_code', videoProviderCode); + const suffix = search.toString() ? `?${search.toString()}` : ''; + return this.request(`/episodes/${episodeId}/storyboard/visual-board-plan${suffix}`); + } + confirmStoryboard(episodeId: string) { return this.request<{ shots: SafeStoryboardShot[]; next_step: string }>( `/episodes/${episodeId}/storyboard/confirm`, @@ -1080,17 +3259,21 @@ export class UserApiClient { ); } - generateShotImages(episodeId: string) { + generateShotImages(episodeId: string, options: { provider_code?: string } = {}) { return this.request(`/episodes/${episodeId}/shot-images/generate`, { method: 'POST', - body: { image_type: 'final', only_missing: true } + body: { image_type: 'final', only_missing: true, provider_code: options.provider_code || undefined } }); } - generateAudio(episodeId: string, options: { force?: boolean } = {}) { + generateAudio(episodeId: string, options: { force?: boolean; voice_provider_code?: string } = {}) { return this.request(`/episodes/${episodeId}/audio/generate`, { method: 'POST', - body: { dialogue_mode: 'mixed', force: options.force ?? false } + body: { + dialogue_mode: 'mixed', + force: options.force ?? false, + voice_provider_code: options.voice_provider_code || undefined + } }); } @@ -1101,6 +3284,7 @@ export class UserApiClient { voice?: string; voice_style?: string; speech_speed?: number | string | null; + voice_provider_code?: string; } ) { return this.request(`/episodes/${episodeId}/audio/segments/${segmentIndex}/retry`, { @@ -1127,10 +3311,10 @@ export class UserApiClient { return this.request(`/projects/${projectId}/live-action/actor-profiles`); } - generateActorProfiles(projectId: string) { + generateActorProfiles(projectId: string, options: { text_provider_code?: string } = {}) { return this.request<{ actor_profiles: SafeActorProfile[]; next_step: string }>( `/projects/${projectId}/live-action/actor-profiles/generate`, - { method: 'POST', body: {} } + { method: 'POST', body: { text_provider_code: options.text_provider_code || undefined } } ); } @@ -1138,17 +3322,35 @@ export class UserApiClient { return this.request(`/episodes/${episodeId}/live-action/shots`); } - prepareLiveActionShots(episodeId: string) { - return this.request<{ shots: SafeLiveActionShot[]; next_step: string }>( - `/episodes/${episodeId}/live-action/shots/prepare`, - { method: 'POST', body: {} } + getLiveActionShotAssetPlans(episodeId: string, providerCode?: string) { + const query = providerCode ? `?provider_code=${encodeURIComponent(providerCode)}` : ''; + + return this.request( + `/episodes/${episodeId}/live-action/shot-asset-plans${query}` ); } - generateLiveActionKeyframes(episodeId: string) { + prepareLiveActionShots(episodeId: string, options: { text_provider_code?: string } = {}) { + return this.request<{ shots: SafeLiveActionShot[]; next_step: string }>( + `/episodes/${episodeId}/live-action/shots/prepare`, + { method: 'POST', body: { text_provider_code: options.text_provider_code || undefined } } + ); + } + + generateLiveActionKeyframes( + episodeId: string, + options: { provider_code?: string; image_provider_code?: string; force?: boolean } = {} + ) { return this.request<{ assets: SafeAsset[]; next_step: string }>( `/episodes/${episodeId}/live-action/keyframes/generate`, - { method: 'POST', body: {} } + { + method: 'POST', + body: { + force: options.force ?? false, + provider_code: options.provider_code || undefined, + image_provider_code: options.image_provider_code || undefined + } + } ); } @@ -1156,6 +3358,26 @@ export class UserApiClient { return this.request(`/episodes/${episodeId}/live-action/video-clips`); } + listLiveActionGenerationPlans(episodeId: string) { + return this.request<{ plans: SafeShotGenerationPlan[] }>( + `/episodes/${episodeId}/live-action/generation-plans` + ); + } + + compareLiveActionGenerationPlans( + episodeId: string, + basePlanId: string, + targetPlanId: string + ) { + const query = new URLSearchParams({ + base_plan_id: basePlanId, + target_plan_id: targetPlanId + }); + return this.request( + `/episodes/${episodeId}/live-action/generation-plans/compare?${query.toString()}` + ); + } + listLiveActionVideoProviders() { return this.request('/live-action/video-providers'); } @@ -1173,6 +3395,8 @@ export class UserApiClient { payload: { provider_code?: string; confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; max_cost_per_clip?: number | string | null; shot_id?: string; action_beat_mode?: boolean | string; @@ -1183,6 +3407,12 @@ export class UserApiClient { if (payload.provider_code) query.set('provider_code', payload.provider_code); if (payload.confirm_real_video !== undefined) query.set('confirm_real_video', String(payload.confirm_real_video)); + if (payload.allow_seedance_real_person_test !== undefined) { + query.set('allow_seedance_real_person_test', String(payload.allow_seedance_real_person_test)); + } + if (payload.allow_character_reference_fallback !== undefined) { + query.set('allow_character_reference_fallback', String(payload.allow_character_reference_fallback)); + } if (payload.shot_id) query.set('shot_id', payload.shot_id); if (payload.action_beat_mode !== undefined) query.set('action_beat_mode', String(payload.action_beat_mode)); if (payload.action_beat_count !== undefined && payload.action_beat_count !== null && payload.action_beat_count !== '') { @@ -1205,12 +3435,98 @@ export class UserApiClient { ); } + generateLiveActionShotKeyframe( + episodeId: string, + shotId: string, + payload: { provider_code?: string; image_provider_code?: string; force?: boolean } = {} + ) { + return this.request<{ shot: SafeLiveActionShot; keyframe: SafeAsset; reused: boolean; next_step: string }>( + `/episodes/${episodeId}/live-action/shots/${shotId}/keyframe/generate`, + { + method: 'POST', + body: { + force: payload.force ?? false, + provider_code: payload.provider_code || undefined, + image_provider_code: payload.image_provider_code || undefined + } + } + ); + } + + approveLiveActionShotKeyframe( + episodeId: string, + shotId: string, + payload: { + provider_code?: string; + resolution?: string; + aspect_ratio?: string; + confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; + generate_audio?: boolean; + max_cost_per_clip?: number | string | null; + action_beat_mode?: boolean | string; + action_beat_count?: number | string | null; + } = {} + ) { + return this.request<{ + shot: SafeLiveActionShot; + keyframe: SafeAsset; + generation_plan: SafeShotGenerationPlan; + next_step: string; + }>( + `/episodes/${episodeId}/live-action/shots/${shotId}/keyframe/approve`, + { method: 'POST', body: payload } + ); + } + + freezeLiveActionShotGenerationPlan( + episodeId: string, + shotId: string, + payload: { + provider_code?: string; + resolution?: string; + aspect_ratio?: string; + confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; + generate_audio?: boolean; + max_cost_per_clip?: number | string | null; + action_beat_mode?: boolean | string; + action_beat_count?: number | string | null; + } = {} + ) { + return this.request<{ + generation_plan: SafeShotGenerationPlan; + next_step: string; + }>( + `/episodes/${episodeId}/live-action/shots/${shotId}/generation-plan/freeze`, + { method: 'POST', body: payload } + ); + } + + updateLiveActionShotPrompt( + episodeId: string, + shotId: string, + payload: { + live_action_desc?: string; + video_prompt?: string; + } + ) { + return this.request<{ shot: SafeLiveActionShot; next_step: string }>( + `/episodes/${episodeId}/live-action/shots/${shotId}/prompt`, + { method: 'POST', body: payload } + ); + } + generateLiveActionShotVideoClip( episodeId: string, shotId: string, payload: { provider_code?: string; confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; force?: boolean; max_cost_per_clip?: number | string | null; action_beat_mode?: boolean | string; @@ -1218,9 +3534,12 @@ export class UserApiClient { } = {} ) { return this.request<{ - video_clip: SafeVideoClip; + video_clip: SafeVideoClip | null; + pending_task?: SafeRenderTask | null; + pending_tasks?: SafeRenderTask[]; preflight: LiveActionPreflightReport; reused: boolean; + submitted_count?: number; next_step: string; }>( `/episodes/${episodeId}/live-action/shots/${shotId}/video-clip/generate`, @@ -1233,13 +3552,22 @@ export class UserApiClient { payload: { provider_code?: string; confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; force?: boolean; max_cost_per_clip?: number | string | null; action_beat_mode?: boolean | string; action_beat_count?: number | string | null; } = {} ) { - return this.request<{ video_clips: SafeVideoClip[]; next_step: string }>( + return this.request<{ + video_clips: SafeVideoClip[]; + pending_tasks?: SafeRenderTask[]; + reused_count?: number; + submitted_count?: number; + pending_count?: number; + next_step: string; + }>( `/episodes/${episodeId}/live-action/video-clips/generate`, { method: 'POST', body: payload } ); @@ -1250,6 +3578,8 @@ export class UserApiClient { payload: { provider_code?: string; confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; max_cost_per_clip?: number | string | null; action_beat_mode?: boolean | string; action_beat_count?: number | string | null; @@ -1267,6 +3597,8 @@ export class UserApiClient { auto_repair?: boolean; min_quality_score?: number | string | null; confirm_real_video?: boolean; + allow_seedance_real_person_test?: boolean | string; + allow_character_reference_fallback?: boolean | string; max_cost_per_clip?: number | string | null; } = {} ) { @@ -1290,10 +3622,60 @@ export class UserApiClient { ); } - renderLiveActionEpisode(episodeId: string) { + selectLiveActionVideoClipCandidate(clipId: string) { + return this.request<{ shot: SafeLiveActionShot; video_clip: SafeVideoClip; next_step: string }>( + `/live-action/video-clips/${clipId}/select-candidate`, + { method: 'POST' } + ); + } + + renderLiveActionEpisode( + episodeId: string, + payload: { + render_title?: string; + include_audio?: boolean; + include_source_audio?: boolean; + include_subtitle?: boolean; + include_bgm?: boolean; + include_sfx?: boolean; + include_ambient_sfx?: boolean; + fallback_sfx_when_no_source_audio?: boolean; + soft_stitch?: boolean | string; + audio_text_mode?: 'auto' | 'dialogue' | 'title'; + subtitle_mode?: 'auto' | 'dialogue' | 'title' | 'shot'; + max_chars_per_line?: number | string | null; + } = {} + ) { return this.request<{ asset: SafeAsset; reused: boolean; next_step: string }>( `/episodes/${episodeId}/live-action/render`, - { method: 'POST', body: { force: true } } + { method: 'POST', body: { force: true, ...payload } } + ); + } + + generateLiveActionMusicPackage( + episodeId: string, + payload: { + provider_code?: string; + music_provider_code?: string; + force?: boolean; + } = {} + ) { + return this.request<{ + music_bible: unknown; + tracks: Array<{ + track_code: string; + title: string; + reused: boolean; + asset: SafeAsset; + task_id: string; + provider_code: string | null; + cost_actual: number | null; + }>; + track_count: number; + next_step: string; + }>( + `/episodes/${episodeId}/live-action/music/generate`, + { method: 'POST', body: payload } ); } @@ -1301,6 +3683,26 @@ export class UserApiClient { return this.request(`/episodes/${episodeId}/media-assets`); } + getAsset(assetId: string) { + return this.request(`/assets/${assetId}`); + } + + async getAssetPreviewUrl(assetId: string, options: { force?: boolean } = {}) { + const cached = options.force ? null : this.readCachedAssetPreviewUrl(assetId); + if (cached) return cached; + + const result = await this.request<{ asset: SafeAsset; url: string; expires_in_seconds: number }>( + `/assets/${assetId}/preview-url` + ); + const localized = { + ...result, + url: this.localizePreviewUrl(result.url) + }; + + this.writeCachedAssetPreviewUrl(assetId, localized); + return localized; + } + listProjectTasks(projectId: string) { return this.request<{ tasks: SafeRenderTask[]; total: number; limit: number }>( `/projects/${projectId}/tasks?limit=50` @@ -1318,7 +3720,7 @@ export class UserApiClient { if (contentType.includes('application/json')) { const text = await response.text(); - const rawPayload = text ? (JSON.parse(text) as unknown) : null; + const rawPayload = parseJsonPayload(text, response.status, contentType); const payload = rawPayload ? await this.crypto.decryptResponse< ApiEnvelope<{ @@ -1354,6 +3756,69 @@ export class UserApiClient { return { blob, filename }; } + private localizePreviewUrl(url: string) { + if (!this.baseUrl.startsWith('/')) return url; + + try { + const parsed = new URL(url); + return `${parsed.pathname}${parsed.search}${parsed.hash}`; + } catch { + return url; + } + } + + private assetPreviewCacheKey(assetId: string) { + return `ai_manga_asset_preview_url:${assetId}`; + } + + private readCachedAssetPreviewUrl(assetId: string) { + if (typeof window === 'undefined') return null; + + try { + const raw = window.localStorage.getItem(this.assetPreviewCacheKey(assetId)); + if (!raw) return null; + + const cached = JSON.parse(raw) as { + asset: SafeAsset; + url: string; + expiresAt: number; + }; + if (!cached.url || !cached.expiresAt || cached.expiresAt <= Date.now() + 30_000) { + window.localStorage.removeItem(this.assetPreviewCacheKey(assetId)); + return null; + } + + return { + asset: cached.asset, + url: cached.url, + expires_in_seconds: Math.max(0, Math.floor((cached.expiresAt - Date.now()) / 1000)) + }; + } catch { + window.localStorage.removeItem(this.assetPreviewCacheKey(assetId)); + return null; + } + } + + private writeCachedAssetPreviewUrl( + assetId: string, + result: { asset: SafeAsset; url: string; expires_in_seconds: number } + ) { + if (typeof window === 'undefined' || !result.url || result.expires_in_seconds <= 30) return; + + try { + window.localStorage.setItem( + this.assetPreviewCacheKey(assetId), + JSON.stringify({ + asset: result.asset, + url: result.url, + expiresAt: Date.now() + result.expires_in_seconds * 1000 + }) + ); + } catch { + // Preview caching is a best-effort speed-up; playback should still work without it. + } + } + private async request(path: string, options: { method?: string; body?: RequestBody } = {}) { const isForm = options.body instanceof FormData; const method = (options.method ?? 'GET').toUpperCase(); @@ -1363,39 +3828,53 @@ export class UserApiClient { !isForm && (options.body !== undefined || shouldSendEmptyJsonBody) ? (options.body ?? {}) : undefined; - const encryptedBody = - bodyPayload !== undefined ? await this.crypto.encryptBody(bodyPayload) : null; - const encryptedHeaders = encryptedBody?.headers ?? (await this.crypto.encryptionHeaders()); - const headers: Record = { - ...encryptedHeaders, - ...this.authHeaders(), - ...(isForm ? {} : { 'content-type': 'application/json' }) - }; - let body: BodyInit | undefined; - if (encryptedBody) { - body = JSON.stringify(encryptedBody.body); - } else if (isForm) { - body = options.body as FormData; - } else if (options.body) { - body = JSON.stringify(options.body); - } - const response = await fetch(`${this.baseUrl}${path}`, { - method, - headers, - body - }); - - const text = await response.text(); - const rawPayload = text ? (JSON.parse(text) as unknown) : null; - const payload = rawPayload - ? await this.crypto.decryptResponse>(rawPayload) + const inspectionId = isInspectableAiRequest(path, method) + ? beginAiRequestInspection({ path, method, body: isForm ? options.body : bodyPayload }) : null; - if (!response.ok || !payload || payload.code !== 0) { - throw new Error(friendlyApiMessage(payload?.message) || `请求失败:HTTP ${response.status}`); - } + try { + const encryptedBody = + bodyPayload !== undefined ? await this.crypto.encryptBody(bodyPayload) : null; + const encryptedHeaders = encryptedBody?.headers ?? (await this.crypto.encryptionHeaders()); + const headers: Record = { + ...encryptedHeaders, + ...this.authHeaders(), + ...(isForm ? {} : { 'content-type': 'application/json' }) + }; + let body: BodyInit | undefined; + if (encryptedBody) { + body = JSON.stringify(encryptedBody.body); + } else if (isForm) { + body = options.body as FormData; + } else if (options.body) { + body = JSON.stringify(options.body); + } + const response = await fetch(`${this.baseUrl}${path}`, { + method, + headers, + body + }); - return payload.data; + const contentType = response.headers.get('content-type') ?? ''; + const text = await response.text(); + const rawPayload = parseJsonPayload(text, response.status, contentType); + const payload = rawPayload + ? await this.crypto.decryptResponse>(rawPayload) + : null; + + if (!response.ok || !payload || payload.code !== 0) { + throw new Error(friendlyApiMessage(payload?.message) || `请求失败:HTTP ${response.status}`); + } + + completeAiRequestInspection(inspectionId, { + requestId: payload.request_id, + responseData: payload.data + }); + return payload.data; + } catch (error) { + failAiRequestInspection(inspectionId, error); + throw error; + } } private authHeaders(): Record { diff --git a/user-app/src/components/production/SplusProductionPipeline.vue b/user-app/src/components/production/SplusProductionPipeline.vue new file mode 100644 index 0000000..9a9d98e --- /dev/null +++ b/user-app/src/components/production/SplusProductionPipeline.vue @@ -0,0 +1,1377 @@ + + + + + diff --git a/user-app/src/components/tools/CreativeTools.vue b/user-app/src/components/tools/CreativeTools.vue new file mode 100644 index 0000000..4585ca3 --- /dev/null +++ b/user-app/src/components/tools/CreativeTools.vue @@ -0,0 +1,1395 @@ + + + + + diff --git a/user-app/src/pages/index/index.vue b/user-app/src/pages/index/index.vue index 178b80e..3551690 100644 --- a/user-app/src/pages/index/index.vue +++ b/user-app/src/pages/index/index.vue @@ -1,33 +1,72 @@