From 9703714b6962f23b06e00f0a3f900b4350c44679 Mon Sep 17 00:00:00 2001 From: saturn Date: Thu, 2 Apr 2026 17:39:16 +0800 Subject: [PATCH] feat: refine UI, improve UX, optimize the analysis pipeline, and add character standing positions --- .../agent_cinematographer.en.txt | 8 +- .../agent_cinematographer.zh.txt | 13 +- .../agent_shot_variant_generate.en.txt | 3 +- .../agent_shot_variant_generate.zh.txt | 3 +- .../agent_storyboard_detail.en.txt | 12 +- .../agent_storyboard_detail.zh.txt | 11 +- .../agent_storyboard_insert.en.txt | 9 +- .../agent_storyboard_insert.zh.txt | 11 +- .../agent_storyboard_plan.en.txt | 11 +- .../agent_storyboard_plan.zh.txt | 23 +- .../novel-promotion/location_create.en.txt | 20 +- .../novel-promotion/location_create.zh.txt | 19 +- .../location_description_update.en.txt | 11 +- .../location_description_update.zh.txt | 14 +- .../novel-promotion/location_modify.en.txt | 12 +- .../novel-promotion/location_modify.zh.txt | 16 +- .../location_regenerate.en.txt | 10 +- .../location_regenerate.zh.txt | 14 + .../novel-promotion/select_location.en.txt | 22 +- .../novel-promotion/select_location.zh.txt | 22 + .../novel-promotion/single_panel_image.en.txt | 11 +- .../novel-promotion/single_panel_image.zh.txt | 4 + messages/en/home.json | 1 + messages/en/storyboard.json | 17 +- messages/en/workspace.json | 7 +- messages/zh/home.json | 1 + messages/zh/storyboard.json | 17 +- messages/zh/workspace.json | 7 +- .../migration.sql | 5 + prisma/schema.prisma | 2 + prisma/schema.sqlit.prisma | 2 + src/app/[locale]/home/page.tsx | 22 +- .../NovelPromotionWorkspace.tsx | 2 - .../components/NovelInputStage.tsx | 108 +--- .../components/PanelEditForm.tsx | 4 +- .../components/WorkspaceHeaderShell.tsx | 27 +- .../components/WorkspaceRunStreamConsoles.tsx | 4 +- .../components/assets/AddLocationModal.tsx | 4 + .../components/assets/CharacterCard.tsx | 15 +- .../components/assets/LocationCard.tsx | 16 +- .../components/storyboard/AIDataModal.tsx | 99 ++-- .../storyboard/AIDataModal.types.ts | 8 +- .../storyboard/AIDataModalFormPane.tsx | 492 ++++++++++++------ .../storyboard/AIDataModalPreviewPane.tsx | 99 +++- .../hooks/useStoryboardAiDataRuntime.ts | 4 +- .../storyboard/hooks/useStoryboardState.ts | 24 +- .../components/storyboard/index.tsx | 2 +- .../storyboard/modal-scroll-lock.ts | 23 + .../useNovelPromotionWorkspaceController.ts | 16 +- .../hooks/useWorkspaceStageNavigation.ts | 29 +- .../asset-hub/components/AddLocationModal.tsx | 4 + .../asset-hub/components/CharacterCard.tsx | 22 +- .../asset-hub/components/LocationCard.tsx | 21 +- src/app/[locale]/workspace/page.tsx | 90 +++- src/app/api/asset-hub/locations/route.ts | 6 + .../[projectId]/location/route.ts | 13 +- src/app/api/projects/route.ts | 42 +- src/app/api/runs/route.ts | 16 + .../ImageGenerationInlineCountButton.tsx | 82 ++- .../shared/assets/LocationCreationModal.tsx | 7 + .../shared/assets/LocationEditModal.tsx | 6 + .../story-input/LongTextDetectionPrompt.tsx | 124 +++++ .../story-input/StoryInputComposer.tsx | 2 +- src/components/ui/CapsuleNav.tsx | 4 +- src/lib/api/read-error-message.ts | 22 + src/lib/asset-utils/ai-design.ts | 29 +- src/lib/assets/services/asset-actions.ts | 15 + .../assets/services/asset-prompt-context.ts | 17 +- .../assets/services/location-backed-assets.ts | 28 +- src/lib/home/create-project-launch.ts | 20 +- src/lib/location-available-slots.ts | 45 ++ src/lib/location-image-prompt.ts | 32 ++ .../insert-panel-prompt-context.ts | 46 ++ src/lib/novel-promotion/panel-ai-data-sync.ts | 1 + .../script-to-storyboard/orchestrator.ts | 56 +- src/lib/novel-promotion/stage-readiness.ts | 74 +++ src/lib/projects/default-name.ts | 11 + src/lib/projects/validation.ts | 82 +++ .../query/hooks/run-stream/recovery-probe.ts | 90 ++++ .../run-stream/run-stream-state-runtime.ts | 91 ++-- src/lib/query/hooks/run-stream/snapshot.ts | 58 --- .../hooks/useScriptToStoryboardRunStream.ts | 26 +- .../query/hooks/useStoryToScriptRunStream.ts | 26 +- .../mutations/asset-hub-creation-mutations.ts | 4 +- .../mutations/asset-hub-update-mutations.ts | 8 +- .../location-management-mutations.ts | 9 +- src/lib/run-runtime/reconcile.ts | 305 +++++++++++ src/lib/run-runtime/recovery.ts | 85 +++ src/lib/run-runtime/service.ts | 36 +- src/lib/run-runtime/types.ts | 2 + src/lib/storyboard-phases.ts | 19 +- src/lib/task/reconcile.ts | 6 +- src/lib/task/submitter.ts | 30 +- .../handlers/analyze-global-persist.ts | 4 + src/lib/workers/handlers/analyze-novel.ts | 4 + .../workers/handlers/asset-hub-ai-design.ts | 1 + .../workers/handlers/asset-hub-ai-modify.ts | 16 +- .../handlers/asset-hub-image-task-handler.ts | 9 +- .../handlers/asset-hub-modify-task-handler.ts | 17 +- .../handlers/image-task-handler-shared.ts | 5 +- .../handlers/image-task-handlers-core.ts | 17 +- .../handlers/location-image-task-handler.ts | 9 +- .../handlers/modify-description-sync.ts | 19 +- .../handlers/panel-image-task-handler.ts | 8 + .../handlers/panel-variant-task-handler.ts | 29 +- .../script-to-storyboard-atomic-retry.ts | 2 + .../handlers/script-to-storyboard-helpers.ts | 246 +++++++++ .../workers/handlers/script-to-storyboard.ts | 147 +----- src/lib/workers/handlers/shot-ai-persist.ts | 7 +- .../handlers/shot-ai-prompt-location.ts | 4 + .../handlers/story-to-script-helpers.ts | 39 +- src/lib/workers/handlers/story-to-script.ts | 139 ++--- src/lib/workers/text.worker.ts | 14 +- src/lib/workflow-engine/dependencies.ts | 90 +--- src/lib/workflow-engine/registry.ts | 214 ++++++++ src/types/project.ts | 2 + .../api/contract/runs-list.route.test.ts | 84 +++ ...project-create-default-audio-model.test.ts | 34 ++ .../reconcile-active-runs.integration.test.ts | 195 +++++++ .../task-reusable-run-reattach.test.ts | 93 ++++ tests/system/text-workflow.system.test.ts | 21 +- .../assets/location-backed-assets.test.ts | 3 + tests/unit/assets/prompt-context.test.ts | 5 +- .../ai-data-modal-preview-pane.test.ts | 34 ++ tests/unit/components/ai-data-modal.test.ts | 48 ++ .../components/capsule-nav-layering.test.ts | 46 ++ ...age-generation-inline-count-button.test.ts | 50 +- .../long-text-detection-prompt.test.ts | 72 +++ .../unit/components/modal-scroll-lock.test.ts | 29 ++ .../workspace-run-stream-consoles.test.ts | 64 +++ tests/unit/helpers/recovery-probe.test.ts | 49 ++ .../helpers/task-submitter-helpers.test.ts | 12 +- tests/unit/home/create-project-launch.test.ts | 1 - tests/unit/home/quick-start-textarea.test.ts | 8 +- tests/unit/location-available-slots.test.ts | 14 + .../insert-panel-prompt-context.test.ts | 33 ++ .../novel-promotion/novel-input-stage.test.ts | 4 + .../novel-promotion/stage-readiness.test.ts | 81 +++ .../optimistic/panel-ai-data-sync.test.ts | 16 + tests/unit/projects/default-name.test.ts | 8 + tests/unit/projects/validation.test.ts | 28 + tests/unit/run-runtime/recovery.test.ts | 76 +++ tests/unit/worker/analyze-novel.test.ts | 2 + tests/unit/worker/asset-hub-ai-design.test.ts | 5 +- tests/unit/worker/asset-hub-ai-modify.test.ts | 2 + .../location-image-task-handler.test.ts | 25 +- .../worker/panel-image-task-handler.test.ts | 35 +- .../worker/panel-variant-task-handler.test.ts | 52 +- ...t-to-storyboard-orchestrator.retry.test.ts | 93 ++++ .../unit/worker/script-to-storyboard.test.ts | 80 +-- .../worker/shot-ai-prompt-location.test.ts | 4 +- tests/unit/worker/story-to-script.test.ts | 7 +- tests/unit/workflow-engine/registry.test.ts | 57 ++ 153 files changed, 4472 insertions(+), 1088 deletions(-) create mode 100644 prisma/migrations/20260328110000_add_location_available_slots/migration.sql create mode 100644 src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/modal-scroll-lock.ts create mode 100644 src/components/story-input/LongTextDetectionPrompt.tsx create mode 100644 src/lib/api/read-error-message.ts create mode 100644 src/lib/location-available-slots.ts create mode 100644 src/lib/location-image-prompt.ts create mode 100644 src/lib/novel-promotion/insert-panel-prompt-context.ts create mode 100644 src/lib/novel-promotion/stage-readiness.ts create mode 100644 src/lib/projects/default-name.ts create mode 100644 src/lib/projects/validation.ts create mode 100644 src/lib/query/hooks/run-stream/recovery-probe.ts delete mode 100644 src/lib/query/hooks/run-stream/snapshot.ts create mode 100644 src/lib/run-runtime/reconcile.ts create mode 100644 src/lib/run-runtime/recovery.ts create mode 100644 src/lib/workflow-engine/registry.ts create mode 100644 tests/integration/api/contract/runs-list.route.test.ts create mode 100644 tests/integration/run-runtime/reconcile-active-runs.integration.test.ts create mode 100644 tests/regression/task-reusable-run-reattach.test.ts create mode 100644 tests/unit/components/ai-data-modal-preview-pane.test.ts create mode 100644 tests/unit/components/ai-data-modal.test.ts create mode 100644 tests/unit/components/capsule-nav-layering.test.ts create mode 100644 tests/unit/components/long-text-detection-prompt.test.ts create mode 100644 tests/unit/components/modal-scroll-lock.test.ts create mode 100644 tests/unit/components/workspace-run-stream-consoles.test.ts create mode 100644 tests/unit/helpers/recovery-probe.test.ts create mode 100644 tests/unit/location-available-slots.test.ts create mode 100644 tests/unit/novel-promotion/insert-panel-prompt-context.test.ts create mode 100644 tests/unit/novel-promotion/stage-readiness.test.ts create mode 100644 tests/unit/projects/default-name.test.ts create mode 100644 tests/unit/projects/validation.test.ts create mode 100644 tests/unit/run-runtime/recovery.test.ts create mode 100644 tests/unit/workflow-engine/registry.test.ts diff --git a/lib/prompts/novel-promotion/agent_cinematographer.en.txt b/lib/prompts/novel-promotion/agent_cinematographer.en.txt index 3701224..9da7242 100644 --- a/lib/prompts/novel-promotion/agent_cinematographer.en.txt +++ b/lib/prompts/novel-promotion/agent_cinematographer.en.txt @@ -29,5 +29,9 @@ Rules: 2. Keep continuity across neighboring panels. 3. Adapt to scene_type and story rhythm. 4. Technical notes must be directly actionable by image/video generation. -5. JSON only, no markdown. -6. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. +5. If characters already carry `slot`, treat it as a preferred placement anchor, not an absolute boundary. +6. When a panel is about movement, entry/exit, path traversal, transition space, temporary space, empty space, imagination, dream, memory, or abstract/non-literal space, composition and placement may deviate from a static slot if the shot logic requires it. +7. Treat `slot` as one full placement phrase from the location context, not as a short token, whenever you reference it. +8. Do not shorten, rewrite, summarize, or replace a provided `slot` phrase with a short token. +9. JSON only, no markdown. +10. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. diff --git a/lib/prompts/novel-promotion/agent_cinematographer.zh.txt b/lib/prompts/novel-promotion/agent_cinematographer.zh.txt index 0ffb9cb..8a4692b 100644 --- a/lib/prompts/novel-promotion/agent_cinematographer.zh.txt +++ b/lib/prompts/novel-promotion/agent_cinematographer.zh.txt @@ -129,8 +129,11 @@ 3. 每个元素必须包含 panel_number 字段 4. 使用相对方向(画面左侧/右侧),禁止使用东南西北 5. 角色位置必须与镜头描述一致! -6. 景深根据 shot_type(全景/中景/近景/特写)自动调整 -7. ⚠️ 对话镜头必须使用浅景深(T2.8或更小),并且注明其他人虚化,确保只有说话者脸部清晰 -8. 如果镜头涉及不同场景,灯光和色调要相应调整 -9. 输出要简洁,每个镜头的规则独立完整 -10. ⚠️ JSON安全:所有引号(""''等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 " +6. 如果角色对象中包含 slot,screen_position / posture / facing 应优先参考该位置语义,但 slot 不是绝对硬限制 +7. 当镜头属于移动过程、入口/出口、过渡区域、路径空间、临时位置、空镜、想象空间、梦境、回忆或抽象空间时,可以基于镜头描述自由决定构图与位置,不必强行贴合 slot +8. slot 若被引用,必须视为一条完整的位置描述,禁止缩写、改写、总结或替换成短词 +9. 景深根据 shot_type(全景/中景/近景/特写)自动调整 +10. ⚠️ 对话镜头必须使用浅景深(T2.8或更小),并且注明其他人虚化,确保只有说话者脸部清晰 +11. 如果镜头涉及不同场景,灯光和色调要相应调整 +12. 输出要简洁,每个镜头的规则独立完整 +13. ⚠️ JSON安全:所有引号(""''等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 " diff --git a/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt b/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt index e2fe254..0791f33 100644 --- a/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt +++ b/lib/prompts/novel-promotion/agent_shot_variant_generate.en.txt @@ -33,4 +33,5 @@ Execution rules: 1. Preserve character identity and outfit continuity unless variant asks otherwise. 2. Preserve location continuity. 3. Change framing/angle/composition according to target shot and camera move. -4. Keep one-frame output only, no text overlays. +4. If characters_info or location_asset includes fixed slots / available slots, keep every visible character anchored to the same fixed slot instead of drifting to another area. +5. Keep one-frame output only, no text overlays. diff --git a/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt b/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt index 4538447..55e3d81 100644 --- a/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt +++ b/lib/prompts/novel-promotion/agent_shot_variant_generate.zh.txt @@ -66,7 +66,8 @@ 2. 保持角色外观与参考图一致(服装、发型、体型) 3. 保持场景氛围与参考图一致(室内布置、光线、色调) 4. 改变镜头视角/景别/构图以匹配变体要求 -5. 输出图像比例: {aspect_ratio} +5. 如果角色信息或场景参考中提供了固定站位 / 可站位置,必须保持人物仍然处于同一固定站位,不得随意换边、换前后景或漂移到其他区域 +6. 输出图像比例: {aspect_ratio} ====================================== 【风格要求】 diff --git a/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt b/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt index cc14765..07863ea 100644 --- a/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt +++ b/lib/prompts/novel-promotion/agent_storyboard_detail.en.txt @@ -12,6 +12,8 @@ Location info: Task: For each panel, output a complete panel object with improved cinematic detail. +If any character already has `slot`, prefer preserving it exactly and use it as a preferred placement anchor. +Treat `slot` as one full placement phrase from the location context, not as a short token. Required fields per panel: - panel_number @@ -30,7 +32,7 @@ Output schema example (field names must be preserved): { "panel_number": 1, "description": "panel description", - "characters": [{ "name": "Character", "appearance": "appearance" }], + "characters": [{ "name": "Character", "appearance": "appearance", "slot": "the position beneath the throne steps at the center of the hall" }], "location": "location name", "scene_type": "daily", "source_text": "source text excerpt", @@ -46,5 +48,9 @@ Rules: 2. Keep source_text semantically aligned with input; do not rewrite story meaning. 3. video_prompt should be motion-ready and concrete. 4. Prefer age+gender wording in video_prompt when naming actors in camera directions. -5. Return JSON array only. -6. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. +5. Preserve every input `slot` when the character is stably positioned, and reflect it as a preferred anchor in refined description/video_prompt. +6. `slot` is not an absolute boundary. If the shot is clearly about movement, entry/exit, path traversal, transition space, temporary space, empty space, imagination, dream, memory, or abstract/non-literal space, you may remove `slot` or keep it without forcing a rigid static match. +7. When no `slot` is used, decide placement freely from source text, action flow, spatial logic, and cinematic staging. +8. Do not shorten, rewrite, summarize, or replace a provided `slot` phrase with a short token. +9. Return JSON array only. +10. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. diff --git a/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt b/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt index e0cabd3..972ffc2 100644 --- a/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt +++ b/lib/prompts/novel-promotion/agent_storyboard_detail.zh.txt @@ -5,6 +5,7 @@ - 为每个分镜设计景别、视角、镜头运动 - 撰写video_prompt(用年龄段+性别替代角色名) - ⚠️ 保留输入分镜中的所有原始字段(特别是 source_text,必须原样保留) +- 如果输入角色包含 slot,应优先原样保留,并让 refined description/video_prompt 优先参考该位置 【镜头语言库】 @@ -147,7 +148,7 @@ "camera_move": "固定", "description": "角色A站在桌前,双手撑在桌面上,表情严肃地看着对面的角色B", "video_prompt": "年轻男子站在桌前,双手撑在桌面上,表情严肃,正在说话,镜头固定拍摄", - "characters": [{"name": "角色A", "appearance": "初始形象"}], + "characters": [{"name": "角色A", "appearance": "初始形象", "slot": "皇宫正中龙椅前方台阶下的位置"}], "location": "办公室", "scene_type": "daily", "source_text": "角色A对角色B说:你好" @@ -179,5 +180,9 @@ 8. 根据输入的分镜数量动态处理 9. panel_number、characters、location、scene_type保持不变 10. description可以适当优化,但不要改变核心内容 -11. ⚠️ 必须保留输入分镜中的 source_text 字段,原样输出到结果中,不得遗漏或修改 -12. ⚠️ JSON安全:所有引号(""''等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 " +11. 如果输入中存在 slot,应优先保留并优先参考该位置,但 slot 不是绝对硬边界 +12. 当镜头明显属于移动过程、入口/出口、过渡区域、路径空间、临时位置、空镜、想象空间、梦境、回忆或抽象空间时,可以删除 slot 或保留 slot 但不严格贴合其静态位置 +13. 若不使用 slot,应根据 source_text、动作过程、空间关系与镜头调度自由决定人物位置,不要为了命中 slot 而破坏叙事逻辑 +14. slot 若被保留,必须原样保留为完整位置描述,禁止缩写、改写、总结或替换成短词 +15. ⚠️ 必须保留输入分镜中的 source_text 字段,原样输出到结果中,不得遗漏或修改 +16. ⚠️ JSON安全:所有引号(""''等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 " diff --git a/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt b/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt index 35de49d..f8f40c8 100644 --- a/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt +++ b/lib/prompts/novel-promotion/agent_storyboard_insert.en.txt @@ -23,7 +23,7 @@ Output format (single JSON object only): { "panel_number": 0, "description": "visual description", - "characters": [{ "name": "Character Name", "appearance": "appearance name" }], + "characters": [{ "name": "Character Name", "appearance": "appearance name", "slot": "the position beneath the throne steps at the center of the hall" }], "location": "location name", "scene_type": "daily", "source_text": "source text or transition shot", @@ -37,5 +37,8 @@ Rules: 1. Return one object only (not array). 2. Keep narrative and spatial continuity between previous and next panel. 3. Use valid character and location names from provided context. -4. JSON only, no markdown. -5. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. +4. If location details include available slots, treat them as preferred anchors. Reuse a provided `slot` for characters who are stably positioned in the scene. +5. You may omit `slot` when the inserted panel is mainly about movement, entry/exit, path traversal, transition space, temporary space, empty space, imagination, dream, memory, or abstract/non-literal space. +6. If `slot` is used, it must copy one full placement phrase from the available slots list verbatim. Do not shorten or rename it. +7. JSON only, no markdown. +8. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. diff --git a/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt b/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt index 0dad461..2a172ac 100644 --- a/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt +++ b/lib/prompts/novel-promotion/agent_storyboard_insert.zh.txt @@ -54,7 +54,7 @@ |------|------|------| | panel_number | number | 固定填 0(由系统重新编号) | | description | string | 画面描述:包含角色动作、位置、表情。禁止身份称呼(如"母亲"),使用具体角色名。禁止主观情绪词(如"显得尴尬"),只描述可视化动作。 | -| characters | array | 出现的角色列表,格式:`[{"name": "角色名", "appearance": "形象名"}]`。角色名必须与角色信息中的名字完全一致。形象名从角色信息的形象列表中选择。 | +| characters | array | 出现的角色列表,格式:`[{"name": "角色名", "appearance": "形象名", "slot": "场景位置描述"}]`。角色名必须与角色信息中的名字完全一致。形象名从角色信息的形象列表中选择。如果场景信息中提供了可站位置,应优先为稳定停留的角色选择 slot,并直接复用可站位置列表中的完整位置描述。动态移动、过渡区域、入口出口、空镜、想象空间等情况可以不使用 slot。 | | location | string | 场景名称,必须与场景信息中的名字完全一致 | | scene_type | string | 场景类型,枚举值:`daily`(日常)/ `emotion`(情感)/ `action`(动作)/ `epic`(史诗)/ `suspense`(悬疑) | | source_text | string | 对应的原文片段。可以基于前后镜头的 source_text 推断,或填写"过渡镜头" | @@ -72,6 +72,8 @@ ❌ location 使用不存在的场景名 → ✅ 必须与场景信息完全一致 ❌ 特写镜头使用非固定的镜头运动 → ✅ 特写必须用"固定" ❌ video_prompt 中使用角色名 → ✅ 必须用年龄段+性别 +❌ 稳定停留位置明明适合使用已有 slot,却完全无视场景锚点 → ✅ 优先复用场景可站位置中的 slot +❌ 把 slot 改写成短词、代号、缩写 → ✅ 若使用 slot,必须直接复制可站位置列表中的完整位置描述 ====================================== 【输出格式】 @@ -83,7 +85,7 @@ { "panel_number": 0, "description": "...", - "characters": [{"name": "...", "appearance": "..."}], + "characters": [{"name": "...", "appearance": "...", "slot": "皇宫正中龙椅前方台阶下的位置"}], "location": "...", "scene_type": "...", "source_text": "...", @@ -91,3 +93,8 @@ "camera_move": "...", "video_prompt": "..." } + +补充原则: +- slot 是优先锚点,不是绝对硬边界 +- 当新镜头主要表现角色走动、进入/离开、穿过空间、临时停留、空白空间或想象空间时,可以不使用 slot +- 若不使用 slot,应根据前后镜头、原文空间关系和过渡逻辑自由决定人物位置 diff --git a/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt b/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt index b25aa01..eb50c6a 100644 --- a/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt +++ b/lib/prompts/novel-promotion/agent_storyboard_plan.en.txt @@ -34,7 +34,7 @@ Output format (JSON array only): "panel_number": 1, "description": "visual action description", "characters": [ - { "name": "Character Name", "appearance": "appearance name" } + { "name": "Character Name", "appearance": "appearance name", "slot": "the position beneath the throne steps at the center of the hall" } ], "location": "location name", "scene_type": "daily", @@ -52,5 +52,10 @@ Planning rules: 3. Keep panel transitions smooth and logically continuous. 4. Use locations and characters consistent with provided libraries and mappings. 5. Prefer concrete, visible actions over abstract wording. -6. Return strict JSON only. -7. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. +6. If location context contains available slots, treat them as preferred placement anchors rather than mandatory hard boundaries. +7. Prefer assigning a `slot` by copying one full placement phrase from `available_slots` verbatim when a character is stably positioned in the scene. +8. You may omit `slot` when the shot is mainly about movement, entry/exit, path traversal, transition space, temporary space, empty space, imagination, dream, memory, or abstract/non-literal space. +9. When no `slot` is used, decide placement freely from source text, action flow, and cinematic staging instead of forcing one of the provided slots. +10. Do not shorten, rewrite, summarize, or replace a provided slot phrase with a short token such as `slot_1`, `left`, or `throne_front`. +11. Return strict JSON only. +12. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. diff --git a/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt b/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt index 87ed399..f10a4be 100644 --- a/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt +++ b/lib/prompts/novel-promotion/agent_storyboard_plan.zh.txt @@ -24,9 +24,9 @@ - 质量优先:确保每个镜头都有意义 2. 每个分镜必须包含: - - panel_number: 分镜序号(1, 2, 3...) - - description: 画面描述(人物动作、场景元素、构图要点) - - characters: [{name: "角色名", appearance: "形象名"}] + - panel_number: 分镜序号(1, 2, 3...) + - description: 画面描述(人物动作、场景元素、构图要点) + - characters: [{name: "角色名", appearance: "形象名", slot: "场景固定位置描述"}] - location: 场景名称(从资产库选择) - scene_type: daily/emotion/action/epic/suspense - source_text: 对应原文片段 ⚠️ 必填,不得为空 @@ -317,10 +317,13 @@ Clip信息: 4. 只返回JSON数组,不得有其他文字 5. ⚠️ source_text 必填,不得为空或null 6. 空间关系必须清晰(朝向、阻挡、位置) -7. 镜头连续性:前后镜头要有动作承接 -8. 禁止身份称呼:必须使用资产库中的具体名字 -9. 禁止主观情绪词:只描述可视化动作和状态 -10. 禁止长句单镜头:包含逗号分隔多个动作/对话的长句必须拆分 -11. 对话必须拆分:每段对话至少 2 个镜头(说话者 + 听者反应) -12. ⚠️ 镜头合理性:只描述当前镜头**实际能拍摄到**的角色,特写/反打等拍不到的可省略 -13. ⚠️ JSON安全:原文中的所有引号(""''「」等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 " +7. 如果场景描述中提供了可站位置列表,应将其视为优先位置锚点,而不是强制覆盖所有可能位置的硬边界 +8. 当角色在场景中稳定停留时,优先从可站位置列表中选择 slot;slot 字段的值必须直接复制完整位置描述,禁止缩写、改写、总结或替换成短词 +9. 当镜头主要表现角色移动、进入/离开、穿过空间、过渡区域、路径空间、临时空间、空白空间、想象/梦境/回忆/抽象空间时,可以不使用 slot,并根据原文和镜头逻辑自由决定位置 +10. 镜头连续性:前后镜头要有动作承接 +11. 禁止身份称呼:必须使用资产库中的具体名字 +12. 禁止主观情绪词:只描述可视化动作和状态 +13. 禁止长句单镜头:包含逗号分隔多个动作/对话的长句必须拆分 +14. 对话必须拆分:每段对话至少 2 个镜头(说话者 + 听者反应) +15. ⚠️ 镜头合理性:只描述当前镜头**实际能拍摄到**的角色,特写/反打等拍不到的可省略 +16. ⚠️ JSON安全:原文中的所有引号(""''「」等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 " diff --git a/lib/prompts/novel-promotion/location_create.en.txt b/lib/prompts/novel-promotion/location_create.en.txt index 29e25dd..164867f 100644 --- a/lib/prompts/novel-promotion/location_create.en.txt +++ b/lib/prompts/novel-promotion/location_create.en.txt @@ -7,13 +7,23 @@ User request: Rules: 1. Output in English only. 2. Start with scene name in this format: "[Scene Name] ..." -3. Describe a wide, clear environment with spatial layout and key objects. -4. Mention lighting direction and atmosphere. -5. No protagonist actions or dialogue. -6. If crowd is implied by context, use generic crowd terms only (guests, pedestrians, audience). +3. Describe a wide, complete environment with controllable spatial layout, key structures, and visible depth. +4. Make foreground, midground, and background explicit. +5. Define at least 3 clear anchor objects or anchor areas and make the nearby open space visible. +6. If the user input is generic, such as 「classroom」 or 「office」, proactively make it specific enough for stable image layout instead of staying generic. +7. Mention lighting direction and atmosphere. +8. No protagonist actions or dialogue. +9. If crowd is implied by context, use generic crowd terms only (guests, pedestrians, audience). +10. Also generate 2-6 fixed `available_slots` as complete descriptive placement phrases tied to concrete scene anchors. +11. Do not mention posture, action, or emotion in `available_slots`. Describe position only. +12. Every anchor mentioned in `available_slots` must appear clearly in the scene prompt. Output format: Return JSON only. ⚠️ JSON SAFETY: All quotation marks MUST be converted to corner brackets「」in JSON string values: { - "prompt": "[Scene Name] environment description" + "prompt": "[Scene Name] environment description", + "available_slots": [ + "the position beside the left edge of the dining table", + "the open space just inside the doorway against the wall" + ] } diff --git a/lib/prompts/novel-promotion/location_create.zh.txt b/lib/prompts/novel-promotion/location_create.zh.txt index 0d8519d..c5c25c0 100644 --- a/lib/prompts/novel-promotion/location_create.zh.txt +++ b/lib/prompts/novel-promotion/location_create.zh.txt @@ -2,7 +2,7 @@ 【场景生成要求(用于出图,中文描述)】 -1. 生成1条中文环境描述(60-120字),像真实摄影场景一样描述 +1. 生成1条中文环境描述(80-140字),像真实摄影场景一样描述,必须足够具体到可以稳定控制画面布局 2. **开头必须明确写明场景名称**: - 描述开头必须以"【场景名称】"的形式标注空间属性 @@ -14,6 +14,10 @@ - 材质要具体(深棕色实木地板、青灰色石砖墙、做旧铁艺栏杆) - 物品要有使用痕迹和生活气息(桌上散落的书籍、墙角堆放的杂物、窗台晒干的植物) - 光线要写清楚来源和效果(午后阳光斜照进来在地板上拉出长影、暖黄色壁灯打在墙面上) + - 必须写出完整空间结构,不要只写泛场景名词 + - 必须写出前景/中景/背景或近处/中部/远处层次 + - 必须写出至少3个清晰可见的关键锚点及其周边空位 + - 如果用户输入很泛(如「学校教室」「办公室」),你必须主动把它具体化为可控构图,而不是停留在泛描述 4. 禁止:不写主角人物具体动作、不写画风、不写"温馨""优雅"等抽象词 @@ -22,9 +26,20 @@ - 人群描述示例:"大厅中宾客三两成群"、"街道上行人往来"、"座位上零散坐着几位观众" - 如果是私密空间或用户明确要求空镜,则不添加人群 +6. 额外生成 2-6 个该场景的固定可站位置: + - 每个位置必须是一条完整的位置描述短语,而不是短词 + - 每个位置必须依附于明确的场景锚物或区域 + - 位置描述中禁止写人物姿态、动作、情绪,只写空间位置 + - 这些位置里提到的锚点必须在场景描述中真实出现 + - 示例:饭桌左侧靠桌边的位置、教室后排靠窗那组课桌外侧的位置、皇宫正中龙椅前方台阶下的位置 + 以下是用户的生成指令:{user_input} 只返回以下json格式,禁止返回一切除json以外的多余内容。⚠️ 所有引号(""''等)在 JSON 字符串值中必须替换为「」,严禁出现未转义的英文双引号 "。 { - "prompt":"「场景名称」场景描述内容" + "prompt":"「场景名称」场景描述内容", + "available_slots":[ + "饭桌左侧靠桌边的位置", + "门口内侧靠墙的位置" + ] } diff --git a/lib/prompts/novel-promotion/location_description_update.en.txt b/lib/prompts/novel-promotion/location_description_update.en.txt index 66bf4c9..9624b1a 100644 --- a/lib/prompts/novel-promotion/location_description_update.en.txt +++ b/lib/prompts/novel-promotion/location_description_update.en.txt @@ -18,9 +18,18 @@ Rules: 2. Return one complete updated description in English. 3. Keep scene name at the beginning: "[{location_name}] ..." 4. No protagonist actions or story narration. +5. Keep the scene spatially specific, with visible structure, depth, and stable anchors. +6. Also regenerate 2-6 `available_slots` that match the updated scene. +7. Each `available_slots` item must be one complete descriptive placement phrase, not a short token and not an object. +8. Do not mention posture, action, or emotion in `available_slots`. +9. Every anchor mentioned in `available_slots` must also appear clearly in the updated scene description. Output format: Return JSON only. ⚠️ JSON SAFETY: All quotation marks MUST be converted to corner brackets「」in JSON string values: { - "prompt": "updated location description" + "prompt": "updated location description", + "available_slots": [ + "the outer-side position beside the rear window desks", + "the open floor directly below the center of the blackboard" + ] } diff --git a/lib/prompts/novel-promotion/location_description_update.zh.txt b/lib/prompts/novel-promotion/location_description_update.zh.txt index 89fea92..7a213ee 100644 --- a/lib/prompts/novel-promotion/location_description_update.zh.txt +++ b/lib/prompts/novel-promotion/location_description_update.zh.txt @@ -27,10 +27,20 @@ 6. 保留未被修改的原有特征 7. 遵循以下描述规范: - 只描述场景本身,禁止描述人物 - - 使用中文输出,长度 50-100 字 + - 使用中文输出,长度 80-140 字 + - 必须让空间结构、关键锚点、前后层次具体可见,不能退化成泛场景描述 + - 若用户修改后引入新的关键锚点或删掉旧锚点,描述必须同步更新 +8. 同时重新生成 2-6 个固定可站位置,且必须与更新后的场景描述一致 +9. 每个可站位置必须是一条完整的位置描述短语,不是短词,不是对象 +10. 可站位置中禁止写人物姿态、动作、情绪,只写位置 +11. 可站位置里提到的关键锚点必须在更新后的场景描述中明确出现 【输出格式】 只返回JSON格式,禁止返回任何其他内容。⚠️ 所有引号(""''等)在 JSON 字符串值中必须替换为「」,严禁出现未转义的英文双引号 ": { - "prompt": "「场景名」更新后的完整场景描述" + "prompt": "「场景名」更新后的完整场景描述", + "available_slots":[ + "教室后排靠窗那组课桌外侧的位置", + "讲台前方黑板正下方的位置" + ] } diff --git a/lib/prompts/novel-promotion/location_modify.en.txt b/lib/prompts/novel-promotion/location_modify.en.txt index d5d1168..ef7e664 100644 --- a/lib/prompts/novel-promotion/location_modify.en.txt +++ b/lib/prompts/novel-promotion/location_modify.en.txt @@ -16,9 +16,19 @@ Rules: 3. Output in English only. 4. Start with scene name: "[{location_name}] ..." 5. No protagonist actions, dialogue, or narrative plot. +6. Make the scene spatially specific enough for controllable image generation, with visible structure, depth, and stable anchors. +7. Keep at least 3 clear anchor objects or anchor areas visible in the modified scene. +8. Regenerate 2-6 `available_slots` that stay consistent with the modified scene. +9. Each `available_slots` item must be one complete descriptive placement phrase, not a short token and not an object. +10. Do not mention posture, action, or emotion in `available_slots`. +11. Every anchor mentioned in `available_slots` must also appear in the modified scene description. Output format: Return JSON only. ⚠️ JSON SAFETY: All quotation marks MUST be converted to corner brackets「」in JSON string values: { - "prompt": "modified location description" + "prompt": "modified location description", + "available_slots": [ + "the position beneath the throne steps at the center of the hall", + "the open space between the left column and the long table" + ] } diff --git a/lib/prompts/novel-promotion/location_modify.zh.txt b/lib/prompts/novel-promotion/location_modify.zh.txt index b196dbd..a6ec590 100644 --- a/lib/prompts/novel-promotion/location_modify.zh.txt +++ b/lib/prompts/novel-promotion/location_modify.zh.txt @@ -45,6 +45,9 @@ * ✅ 好:"木质书桌"/"灰色布艺沙发"/"白色窗帘" * ❌ 差:"优雅的家具"/"舒适的环境"/"温馨的氛围" - 描述固定元素,不写主角人物具体动作、情绪 + - 必须让空间结构足够具体,不能只剩泛场景标签 + - 必须让前景/中景/背景或近处/中部/远处关系清楚 + - 必须保留至少 3 个清晰可见、可供后续落位的锚点或区域 - 【人群处理规则】如果用户要求添加人群,或场景本身暗示有人群(如宴会、集市等): * 可以加入模糊人群描述:\"人群\"、\"宾客\"、\"路人\"等 * 示例:\"大厅远处三两宾客交谈\"、\"街角有行人匆匆走过\" @@ -54,11 +57,22 @@ 你的目标是根据用户的修改指令,在原有场景描述的基础上进行修改 +同时你必须重新生成该场景的固定可站位置: +- 输出 2-6 个站位 +- 每个站位必须是一条完整的位置描述短语,不是短词 +- 每个站位必须依附于明确场景锚物或区域 +- 站位中禁止写人物姿态、动作、情绪,只写位置 +- 新站位必须与修改后的场景描述一致,且其中提到的锚点必须在场景描述中出现 + 当前场景描述:{location_input} 用户的修改指令:{user_input} 发送json格式给我,只返回以下json格式,禁止返回一切除json以外的多余内容,注释,文字等等,只返回无任何markdown标识符的纯净json格式。⚠️ 所有引号(""''等)在 JSON 字符串值中必须统一替换为「」,严禁出现未转义的英文双引号 "。json格式如下 { - "prompt":"「场景名」xxxxx" + "prompt":"「场景名」xxxxx", + "available_slots":[ + "皇宫正中龙椅前方台阶下的位置", + "左侧立柱与长案之间的空位" + ] } diff --git a/lib/prompts/novel-promotion/location_regenerate.en.txt b/lib/prompts/novel-promotion/location_regenerate.en.txt index 604e045..2ad4ad3 100644 --- a/lib/prompts/novel-promotion/location_regenerate.en.txt +++ b/lib/prompts/novel-promotion/location_regenerate.en.txt @@ -12,7 +12,11 @@ Requirements: 2. Keep the scene name prefix in each line: "[{location_name}] ..." 3. Output in English only. 4. Keep environment-only description (no protagonist actions). -5. Keep each variant concise and image-generation friendly. +5. Keep each variant specific enough for controllable image generation, with visible structure, depth, and stable anchors. +6. Also generate 2-6 shared `available_slots` for this location. +7. Each `available_slots` item must be one complete descriptive placement phrase, not a short token and not an object. +8. Do not mention posture, action, or emotion in `available_slots`. +9. Every anchor mentioned in `available_slots` must remain valid across all three regenerated descriptions. Output format: Return JSON only. ⚠️ JSON SAFETY: All quotation marks MUST be converted to corner brackets「」in JSON string values: @@ -21,5 +25,9 @@ Return JSON only. ⚠️ JSON SAFETY: All quotation marks MUST be converted to c "[{location_name}] variant 1", "[{location_name}] variant 2", "[{location_name}] variant 3" + ], + "available_slots": [ + "the position beneath the throne steps at the center of the hall", + "the position against the inner wall beside the rear doorway" ] } diff --git a/lib/prompts/novel-promotion/location_regenerate.zh.txt b/lib/prompts/novel-promotion/location_regenerate.zh.txt index 855fee1..10c412c 100644 --- a/lib/prompts/novel-promotion/location_regenerate.zh.txt +++ b/lib/prompts/novel-promotion/location_regenerate.zh.txt @@ -20,6 +20,8 @@ - 材质细节:地面、墙面、物体的材质质感 - 环境元素:植物、天气、装饰物等 - 独特标识:该场景的标志性元素或特殊物件 + - 至少 3 个可供后续固定人物位置的稳定锚点或区域 + - 每个锚点周边可落位的空白区域 5. 描述规范: - 禁止写主角人物具体动作、剧情 @@ -31,11 +33,23 @@ - 【年代一致性】根据场景特征判断年代,建筑、装饰、物品必须符合该年代特征 - 【时间一致性】如场景名包含"白天/黑夜/黄昏"等,描述中的光影必须匹配 +6. 额外输出 2-6 个固定可站位置: + - 与该场景的共通构图一致 + - 每个站位必须是一条完整的位置描述短语,不是短词,不是对象 + - 依附于明确场景锚物或区域 + - 禁止抽象站位 + - 禁止写人物姿态、动作、情绪 + - 站位中提到的锚点必须在三条 descriptions 中都成立 + 【输出格式】只返回以下 JSON,不要任何其他内容。⚠️ 所有引号(""''等)在 JSON 字符串值中必须替换为「」,严禁出现未转义的英文双引号 "。 { "descriptions": [ "「场景名」新描述1(80-150字)", "「场景名」新描述2(80-150字)", "「场景名」新描述3(80-150字)" + ], + "available_slots": [ + "皇宫正中龙椅前方台阶下的位置", + "右后方殿门内侧靠墙的位置" ] } diff --git a/lib/prompts/novel-promotion/select_location.en.txt b/lib/prompts/novel-promotion/select_location.en.txt index 9f7fa9f..2977e0d 100644 --- a/lib/prompts/novel-promotion/select_location.en.txt +++ b/lib/prompts/novel-promotion/select_location.en.txt @@ -17,9 +17,17 @@ For each selected location, generate 3 wide-angle environment descriptions. Each description should: - start with location name in brackets: "[Location Name] ..." - describe spatial layout, depth layers, major objects, and lighting direction +- define at least 3 stable anchor objects or anchor areas that can support later character placement +- make the usable open space around those anchors visually clear - remain environment-only (no named protagonist actions) - use concise, production-ready English +If the source location is generic, such as 「classroom」, 「office」, or 「living room」, you must proactively make it specific enough for controllable image generation: +- define the visible main structure +- define foreground, midground, and background +- define at least 3 stable anchor areas and nearby open space +- avoid vague noun piles + Output format (JSON only): { "locations": [ @@ -28,6 +36,11 @@ Output format (JSON only): "summary": "short usage summary", "has_crowd": false, "crowd_description": "", + "available_slots": [ + "the position beneath the throne steps at the center of the palace hall", + "the open space between the left column and the long table", + "the position against the inner wall beside the rear doorway" + ], "descriptions": [ "[location_name] description 1", "[location_name] description 2", @@ -40,5 +53,10 @@ Output format (JSON only): Strict constraints: 1. JSON only. 2. If no valid location exists, return: {"locations":[]}. -3. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. - \ No newline at end of file +3. Each location must include 2-6 `available_slots`. +4. Each `available_slots` item must be a complete descriptive placement phrase, not a short token and not an object. +5. Slot descriptions must be concrete and reusable, tied to visible anchors such as the outer side of a desk row, the open floor before the blackboard, the inner side of a doorway, the space beside a window wall. +6. Do not mention character posture, action, or emotion inside `available_slots`. Describe position only. +7. Every anchor mentioned in `available_slots` must also appear clearly in the location descriptions. +8. ⚠️ JSON SAFETY: All quotation marks in text (""''「」 etc.) MUST be converted to corner brackets「」in JSON string values. NEVER use raw ASCII double quotes " inside string values. + diff --git a/lib/prompts/novel-promotion/select_location.zh.txt b/lib/prompts/novel-promotion/select_location.zh.txt index 0c23739..f1f6a22 100644 --- a/lib/prompts/novel-promotion/select_location.zh.txt +++ b/lib/prompts/novel-promotion/select_location.zh.txt @@ -72,9 +72,15 @@ - 使用明确的位置词:左侧/右侧/中央/角落/靠窗/远处 - 描述物体之间的空间关系和前后层次 - 5-8件物体,每件都有位置说明 + - 至少 2-3 个后续可作为人物落位锚点的关键物体或区域必须被明确写出,如桌边、门内侧、窗下墙边、讲台前、龙椅前台阶 **光线方向**:光从哪个方向照入,照亮哪些区域 + **可落位空间**(必须体现): + - 必须说明哪些区域留有可供人物站立或出现的空白空间 + - 这些空白区域必须与关键锚点相邻,便于后续固定人物位置 + - 禁止把所有锚点都塞满家具或遮挡物,导致后续无法落人 + 5. 描述规范: - 强调位置关系词:前方、远处、左侧、角落、靠近、深处 - 长度 100-150 字 @@ -108,6 +114,12 @@ 8.如无特殊要求,使用用户输入的语言来进行场景生成,例如输入英文输出偏西方场景,中文则输出偏中国场景,但是原则要按照文字剧本里实际发生的地点为准, +9. 如果原文或用户输入过于泛化(如「学校教室」「办公室」「客厅」),你必须主动将其具体化为可控画面的完整空间: + - 明确主视角下能看到的关键结构 + - 明确前景/中景/背景 + - 明确至少 3 个稳定锚点及其周边空位 + - 禁止只输出泛泛的场景名词堆砌 + 【输出规范(只允许以下 JSON 结构;字段名中文;不得输出任何多余文字)】 { "locations": [ @@ -116,6 +128,11 @@ "summary": "场景简要说明(用途/人物关联,如:张三居住的主卧室、公司高层会议室等)", "has_crowd": true/false, "crowd_description": "人群类型描述(仅当has_crowd为true时填写,如:宴会宾客、集市人群、学生们等)", + "available_slots": [ + "皇宫正中龙椅前方台阶下的位置", + "左侧立柱与长案之间的空位", + "右后方殿门内侧靠墙的位置" + ], "descriptions": [ "「场景名」场景环境描述1(如has_crowd为true则包含人群元素)", "「场景名」场景环境描述2", @@ -127,6 +144,11 @@ 【严格性】 - 若无符合条件的场景,locations数组返回 []。 +- 每个场景必须生成 2-6 个 available_slots,且每个站位都必须具体、可复用、与场景内明确锚物相关。 +- 每个 available_slots 元素必须是一条完整的位置描述短语,不是短词,不是结构化对象。 +- 站位描述必须像「皇宫正中龙椅前方台阶下的位置」「教室后排靠窗那组课桌外侧的位置」这样,直接说明锚物、方位和具体区域。 +- 禁止抽象站位,如「左边」「中间」「角落」;禁止写人物姿态、动作、情绪;只描述位置本身。 +- available_slots 中提到的所有关键锚点,必须在 descriptions 中清楚出现,否则该站位无效,不能输出。 - 只返回上述 JSON;不得输出markdown代码块标记、如```json注释或解释;不得添加未定义字段。 - 每条描述必须遵守长度限制(100-150字);发现超长请自行截断。 - 禁止在 JSON 字符串值中出现英文双引号 "。原文中的所有引号(""''等)必须统一替换为「」。如字符串内确实需要英文双引号,必须转义为 \" diff --git a/lib/prompts/novel-promotion/single_panel_image.en.txt b/lib/prompts/novel-promotion/single_panel_image.en.txt index d753336..2c2ae05 100644 --- a/lib/prompts/novel-promotion/single_panel_image.en.txt +++ b/lib/prompts/novel-promotion/single_panel_image.en.txt @@ -21,7 +21,10 @@ Style requirement: Execution rules: 1. Respect panel composition, character placement, and action logic. -2. Use reference images for style/identity consistency only. -3. Repaint the background according to shot type and angle. -4. If storyboard conflicts with source text, keep narrative logic from source text. -5. Keep final visual style consistent with provided references. +2. If storyboard data contains `slot`, treat it as a preferred placement anchor for consistency, not as an absolute restriction. +3. If location data contains `available_slots`, treat them as typical anchor references rather than a complete map of all valid positions. +4. When the panel description, source text, action flow, or scene nature implies movement, entry/exit, path traversal, transition space, temporary space, empty space, or abstract/non-literal space, do not force the character to remain inside an existing slot. +5. Use reference images for style/identity consistency only. +6. Repaint the background according to shot type and angle. +7. If storyboard conflicts with source text, keep narrative logic from source text. +8. Keep final visual style consistent with provided references. diff --git a/lib/prompts/novel-promotion/single_panel_image.zh.txt b/lib/prompts/novel-promotion/single_panel_image.zh.txt index 390cf3e..5fa8044 100644 --- a/lib/prompts/novel-promotion/single_panel_image.zh.txt +++ b/lib/prompts/novel-promotion/single_panel_image.zh.txt @@ -50,6 +50,10 @@ - 严格按照分镜要求绘制画面 - 禁止添加、删除或重排任何镜头 - 镜头必须与输入完全匹配 +- 如果分镜数据中包含角色 slot,可将其视为优先位置参考,用于保持人物落位一致性,但不是绝对硬限制 +- 如果场景数据中包含 available_slots,应将其理解为典型位置参考,而不是完整空间边界 +- 当镜头描述、原文空间关系、动作过程或场景性质表明人物正在移动、处于过渡区域、入口出口、路径空间、临时空间、空白空间或想象/梦境/回忆/抽象空间时,不要强行把人物锁死在现有 slot 中 +- 最终以镜头叙事正确、空间关系自然、人物位置合理为最高原则 【分镜数据】 {storyboard_text_json_input} diff --git a/messages/en/home.json b/messages/en/home.json index 7afccab..89f5471 100644 --- a/messages/en/home.json +++ b/messages/en/home.json @@ -2,6 +2,7 @@ "title": "From Inspiration to Screen", "subtitle": "Describe your story and let AI generate cinematic short dramas", "inputPlaceholder": "Enter your story idea, novel excerpt, or script outline...", + "defaultProjectName": "New Project {timestamp}", "startCreation": "Start Creating", "recentProjects": "Recent Projects", "viewAll": "View All Projects", diff --git a/messages/en/storyboard.json b/messages/en/storyboard.json index ad37bad..8bc4bd3 100644 --- a/messages/en/storyboard.json +++ b/messages/en/storyboard.json @@ -346,7 +346,22 @@ "actingNotes": "Acting Direction (acting_notes)", "actingTitle": "Acting Direction", "actingDescription": "Performance Notes", - "noActingData": "No acting data" + "noActingData": "No acting data", + "appearance": "Appearance", + "appearanceReadonly": "Appearance (read-only)", + "slot": "Slot", + "slotUnset": "Unset", + "framePosition": "Frame Position", + "screenPosition": "Screen Position", + "actingGuide": "Acting Direction", + "photoEnv": "Photography Settings", + "availableSlots": "Available Slots", + "optional": "Optional", + "reference": "Reference", + "aiRequired": "AI Required", + "characterDetails": "Character Details", + "shotAndScene": "Shot · Scene", + "jsonCheck": "JSON Review" }, "insertModal": { "insertBetween": "Insert between #{before} and #{after}", diff --git a/messages/en/workspace.json b/messages/en/workspace.json index bde568e..acf9440 100644 --- a/messages/en/workspace.json +++ b/messages/en/workspace.json @@ -23,6 +23,11 @@ "createFailed": "Failed to create project", "analysisModelRequiredAfterCreate": "Project created. Please configure default models in Profile first (at minimum an analysis model), or the project cannot be used.", "updateFailed": "Failed to update project", + "validation": { + "nameRequired": "Project name is required.", + "nameTooLong": "Project name cannot exceed 100 characters.", + "descriptionTooLong": "Project description cannot exceed 500 characters." + }, "deleteFailed": "Failed to delete project", "totalProjects": "{count} projects in total", "statsEpisodes": "Episodes", @@ -34,4 +39,4 @@ "link": "Settings Center", "after": "to configure models, or customize them in project settings after creation." } -} \ No newline at end of file +} diff --git a/messages/zh/home.json b/messages/zh/home.json index 75c474c..7bf3b41 100644 --- a/messages/zh/home.json +++ b/messages/zh/home.json @@ -2,6 +2,7 @@ "title": "从灵感到银幕", "subtitle": "描述你想要创作的故事,AI 为你智能生成影视短剧", "inputPlaceholder": "输入你的故事创意、小说片段或剧本大纲...", + "defaultProjectName": "新项目 {timestamp}", "startCreation": "开始创作", "recentProjects": "最近项目", "viewAll": "查看全部项目", diff --git a/messages/zh/storyboard.json b/messages/zh/storyboard.json index dab37e7..9d7dc8a 100644 --- a/messages/zh/storyboard.json +++ b/messages/zh/storyboard.json @@ -346,7 +346,22 @@ "actingNotes": "演技指导 (acting_notes)", "actingTitle": "演技指导", "actingDescription": "表演指令", - "noActingData": "无演技数据" + "noActingData": "无演技数据", + "appearance": "外貌描述", + "appearanceReadonly": "外貌(只读)", + "slot": "站位(slot)", + "slotUnset": "未指定", + "framePosition": "画面站位", + "screenPosition": "屏幕位置", + "actingGuide": "表演指导", + "photoEnv": "摄影环境", + "availableSlots": "场景可用位置", + "optional": "可选", + "reference": "参考", + "aiRequired": "AI 必读", + "characterDetails": "角色详情", + "shotAndScene": "镜头 · 场景", + "jsonCheck": "JSON 核查" }, "insertModal": { "insertBetween": "在 #{before} 和 #{after} 之间插入", diff --git a/messages/zh/workspace.json b/messages/zh/workspace.json index d92ee10..77b19ee 100644 --- a/messages/zh/workspace.json +++ b/messages/zh/workspace.json @@ -23,6 +23,11 @@ "createFailed": "创建项目失败", "analysisModelRequiredAfterCreate": "项目已创建。请先前往个人设置配置默认模型(至少设置分析模型),否则无法使用。", "updateFailed": "更新项目失败", + "validation": { + "nameRequired": "项目名称不能为空。", + "nameTooLong": "项目名称不能超过 100 个字符。", + "descriptionTooLong": "项目描述不能超过 500 个字符。" + }, "deleteFailed": "删除项目失败", "totalProjects": "共 {count} 个项目", "statsEpisodes": "章节数", @@ -34,4 +39,4 @@ "link": "设置中心", "after": "配置模型,或在创建项目后于项目配置中自定义。" } -} \ No newline at end of file +} diff --git a/prisma/migrations/20260328110000_add_location_available_slots/migration.sql b/prisma/migrations/20260328110000_add_location_available_slots/migration.sql new file mode 100644 index 0000000..29394dd --- /dev/null +++ b/prisma/migrations/20260328110000_add_location_available_slots/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE location_images + ADD COLUMN availableSlots TEXT NULL; + +ALTER TABLE global_location_images + ADD COLUMN availableSlots TEXT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a517ed0..1e7af21 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -58,6 +58,7 @@ model LocationImage { locationId String imageIndex Int description String? @db.Text + availableSlots String? @db.Text imageUrl String? @db.Text isSelected Boolean @default(false) createdAt DateTime @default(now()) @@ -899,6 +900,7 @@ model GlobalLocationImage { locationId String imageIndex Int description String? @db.Text + availableSlots String? @db.Text imageUrl String? @db.Text imageMediaId String? imageMedia MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) diff --git a/prisma/schema.sqlit.prisma b/prisma/schema.sqlit.prisma index 0af064e..879be54 100644 --- a/prisma/schema.sqlit.prisma +++ b/prisma/schema.sqlit.prisma @@ -58,6 +58,7 @@ model LocationImage { locationId String imageIndex Int description String? + availableSlots String? imageUrl String? isSelected Boolean @default(false) createdAt DateTime @default(now()) @@ -736,6 +737,7 @@ model GlobalLocationImage { locationId String imageIndex Int description String? + availableSlots String? imageUrl String? imageMediaId String? imageMedia MediaObject? @relation("GlobalLocationImageMedia", fields: [imageMediaId], references: [id], onDelete: SetNull) diff --git a/src/app/[locale]/home/page.tsx b/src/app/[locale]/home/page.tsx index 168d7f0..76b7ca8 100644 --- a/src/app/[locale]/home/page.tsx +++ b/src/app/[locale]/home/page.tsx @@ -17,6 +17,7 @@ import { Link, useRouter } from '@/i18n/navigation' import { apiFetch } from '@/lib/api-fetch' import { expandHomeStory } from '@/lib/home/ai-story-expand' import { createHomeProjectLaunch } from '@/lib/home/create-project-launch' +import { formatDefaultProjectTimestamp } from '@/lib/projects/default-name' import { HOME_QUICK_START_MIN_ROWS } from '@/lib/ui/textarea-height' import AiWriteModal from '@/components/home/AiWriteModal' @@ -52,6 +53,7 @@ export default function HomePage() { const [artStyle, setArtStyle] = useState('american-comic') const [stylePresetValue, setStylePresetValue] = useState(DEFAULT_STYLE_PRESET_VALUE) const [createLoading, setCreateLoading] = useState(false) + const [createError, setCreateError] = useState(null) const [aiWriteOpen, setAiWriteOpen] = useState(false) const [aiWriteLoading, setAiWriteLoading] = useState(false) @@ -92,12 +94,15 @@ export default function HomePage() { // 创建项目并跳转 const handleCreate = async () => { if (!inputValue.trim() || createLoading) return + setCreateError(null) setCreateLoading(true) try { const storyText = inputValue.trim() const result = await createHomeProjectLaunch({ apiFetch, - projectName: storyText.slice(0, 50), + projectName: t('defaultProjectName', { + timestamp: formatDefaultProjectTimestamp(new Date()), + }), storyText, videoRatio, artStyle, @@ -107,7 +112,7 @@ export default function HomePage() { router.push(result.target) } catch (error) { const message = error instanceof Error ? error.message : t('createFailed') - window.alert(message) + setCreateError(message) } finally { setCreateLoading(false) } @@ -245,9 +250,15 @@ export default function HomePage() { { + setInputValue(nextValue) + if (createError) { + setCreateError(null) + } + }} placeholder={t('inputPlaceholder')} minRows={HOME_QUICK_START_MIN_ROWS} + textareaClassName="px-0 pt-0 pb-3 align-top" videoRatio={videoRatio} onVideoRatioChange={setVideoRatio} ratioOptions={ratioOptions} @@ -286,6 +297,11 @@ export default function HomePage() { )} + footer={createError ? ( +

+ {createError} +

+ ) : null} /> diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx index 4d48980..d964b59 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/NovelPromotionWorkspace.tsx @@ -42,13 +42,11 @@ function NovelPromotionWorkspaceContent(props: NovelPromotionWorkspaceProps) { const showStoryToScriptMinBadge = storyToScriptStream.isVisible && - storyToScriptStream.stages.length > 0 && storyToScriptActive && vm.execution.storyToScriptConsoleMinimized const showScriptToStoryboardMinBadge = scriptToStoryboardStream.isVisible && - scriptToStoryboardStream.stages.length > 0 && scriptToStoryboardActive && vm.execution.scriptToStoryboardConsoleMinimized diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx index 02cae75..dde4880 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/NovelInputStage.tsx @@ -9,6 +9,7 @@ import { useTranslations } from 'next-intl' import { useState, useRef, useEffect, useCallback } from 'react' import '@/styles/animations.css' import AiWriteModal from '@/components/home/AiWriteModal' +import LongTextDetectionPrompt from '@/components/story-input/LongTextDetectionPrompt' import StoryInputComposer from '@/components/story-input/StoryInputComposer' import { ART_STYLES, VIDEO_RATIOS } from '@/lib/constants' import TaskStatusInline from '@/components/task/TaskStatusInline' @@ -197,7 +198,7 @@ export default function NovelInputStage({ stylePresetValue={stylePresetValue} onStylePresetChange={setStylePresetValue} stylePresetOptions={STYLE_PRESETS} - textareaClassName="text-base p-5 pb-3" + textareaClassName="px-0 pt-0 pb-3 align-top" primaryAction={( - - {/* 直接创作 — 弱化按钮 */} - - - - - - - )} + setShowLongTextPrompt(false)} + onSmartSplit={() => { + setShowLongTextPrompt(false) + onSmartSplit?.(localText) + }} + onContinue={() => { + setShowLongTextPrompt(false) + onNext() + }} + /> ) } diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/PanelEditForm.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/PanelEditForm.tsx index b5d621d..5a3cdd6 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/PanelEditForm.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/PanelEditForm.tsx @@ -21,7 +21,7 @@ export interface PanelEditData { cameraMove: string | null description: string | null location: string | null - characters: { name: string; appearance: string }[] + characters: { name: string; appearance: string; slot?: string }[] srtStart: number | null srtEnd: number | null duration: number | null @@ -75,7 +75,7 @@ export default function PanelEditForm({ interface CharacterPickerModalProps { projectId: string - currentCharacters: { name: string; appearance: string }[] + currentCharacters: { name: string; appearance: string; slot?: string }[] onSelect: (charName: string, appearance: string) => void onClose: () => void } diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceHeaderShell.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceHeaderShell.tsx index b7a32c3..e243c84 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceHeaderShell.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceHeaderShell.tsx @@ -5,6 +5,7 @@ import { SettingsModal, WorldContextModal } from '@/components/ui/ConfigModals' import WorkspaceTopActions from './WorkspaceTopActions' import type { NovelPromotionPanel } from '@/types/project' import type { CapabilitySelections, ModelCapabilities } from '@/lib/model-config-contract' +import { resolveEpisodeStageArtifacts } from '@/lib/novel-promotion/stage-readiness' interface EpisodeSummary { id: string @@ -164,15 +165,23 @@ export default function WorkspaceHeaderShell({ return ( ({ - id: ep.id, - title: ep.name, - summary: ep.description ?? undefined, - status: { - script: ep.clips?.length ? 'ready' as const : 'empty' as const, - visual: ep.storyboards?.some((sb) => sb.panels?.some((panel) => panel.videoUrl)) ? 'ready' as const : 'empty' as const, - }, - }))} + episodes={sorted.map((ep) => { + const stageArtifacts = resolveEpisodeStageArtifacts({ + novelText: null, + clips: ep.clips || [], + storyboards: ep.storyboards || [], + voiceLines: [], + }) + return { + id: ep.id, + title: ep.name, + summary: ep.description ?? undefined, + status: { + script: stageArtifacts.hasScript ? 'ready' as const : 'empty' as const, + visual: stageArtifacts.hasVideo ? 'ready' as const : 'empty' as const, + }, + } + })} currentId={currentEpisodeId} onSelect={(id) => onEpisodeSelect?.(id)} onAdd={onEpisodeCreate} diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceRunStreamConsoles.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceRunStreamConsoles.tsx index 15a3796..128f380 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceRunStreamConsoles.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/WorkspaceRunStreamConsoles.tsx @@ -65,7 +65,7 @@ export default function WorkspaceRunStreamConsoles({ const showStoryToScriptConsole = storyToScriptStream.isVisible && - (storyToScriptStream.stages.length > 0 || !!storyToScriptStream.errorMessage) + (storyToScriptStream.stages.length > 0 || !!storyToScriptStream.errorMessage || storyToScriptActive) const storyFallbackStatus: LLMStageViewItem['status'] = storyToScriptStream.status === 'failed' ? 'failed' : 'processing' const storyToScriptStages = storyToScriptStream.stages.length > 0 @@ -94,7 +94,7 @@ export default function WorkspaceRunStreamConsoles({ storyToScriptSelectedStage?.status === 'processing' const showScriptToStoryboardConsole = scriptToStoryboardStream.isVisible && - (scriptToStoryboardStream.stages.length > 0 || !!scriptToStoryboardStream.errorMessage) + (scriptToStoryboardStream.stages.length > 0 || !!scriptToStoryboardStream.errorMessage || scriptToStoryboardActive) const storyboardFallbackStatus: LLMStageViewItem['status'] = scriptToStoryboardStream.status === 'failed' ? 'failed' : 'processing' const scriptToStoryboardStages = scriptToStoryboardStream.stages.length > 0 diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/AddLocationModal.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/AddLocationModal.tsx index 9999545..fd759f0 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/AddLocationModal.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/AddLocationModal.tsx @@ -10,6 +10,7 @@ import { useAiCreateProjectLocation, useCreateProjectLocation } from '@/lib/quer import TaskStatusInline from '@/components/task/TaskStatusInline' import { resolveTaskPresentationState } from '@/lib/task/presentation' import { AppIcon } from '@/components/ui/icons' +import type { LocationAvailableSlot } from '@/lib/location-available-slots' interface AddLocationModalProps { projectId: string @@ -58,6 +59,7 @@ export default function AddLocationModal({ const [description, setDescription] = useState('') const [aiInstruction, setAiInstruction] = useState('') const [artStyle, setArtStyle] = useState('american-comic') + const [availableSlots, setAvailableSlots] = useState([]) const [isSubmitting, setIsSubmitting] = useState(false) const [isAiDesigning, setIsAiDesigning] = useState(false) const aiDesigningState = isAiDesigning @@ -87,6 +89,7 @@ export default function AddLocationModal({ userInstruction: aiInstruction, }) setDescription(data.prompt || '') + setAvailableSlots(Array.isArray(data.availableSlots) ? data.availableSlots : []) setAiInstruction('') } catch (error: unknown) { if (getErrorStatus(error) === 402) { @@ -113,6 +116,7 @@ export default function AddLocationModal({ description: description.trim(), artStyle, count: locationGenerationCount, + availableSlots, }) onSuccess() onClose() diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterCard.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterCard.tsx index db8ace2..ffb0a12 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterCard.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/assets/CharacterCard.tsx @@ -140,6 +140,7 @@ export default function CharacterCard({ const imageUrlsWithIndex = rawImageUrls .map((url, idx) => ({ url, originalIndex: idx })) .filter((item) => !!item.url) as { url: string; originalIndex: number }[] + const generatedImageCount = imageUrlsWithIndex.length const hasMultipleImages = imageUrlsWithIndex.length > 1 const selectedIndex = appearance.selectedIndex ?? null @@ -218,22 +219,24 @@ export default function CharacterCard({ <> + <> + + {t('image.regenCountPrefix')} + ) : ( <> {t('image.regenCountPrefix')} )} - suffix={{t('image.regenCountSuffix')}} value={generationCount} options={getImageGenerationCountOptions('character')} onValueChange={setGenerationCount} - onClick={() => onRegenerate(generationCount)} + onClick={() => onRegenerate(generatedImageCount)} disabled={isAppearanceTaskRunning || isAnyTaskRunning || uploadImage.isPending} - ariaLabel={t('image.regenCountAriaLabel')} - className="inline-flex h-6 items-center gap-0.5 rounded px-1 hover:bg-[var(--glass-tone-info-bg)] transition-colors disabled:opacity-50" - selectClassName="appearance-none bg-transparent border-0 pl-0 pr-3 text-[10px] font-semibold text-[var(--glass-tone-info-fg)] outline-none cursor-pointer leading-none transition-colors" + showCountControl={false} + ariaLabel={t('image.regenCountPrefix')} + className="inline-flex h-6 items-center justify-center rounded-md px-1.5 hover:bg-[var(--glass-tone-info-bg)] transition-colors disabled:opacity-50" /> {onUndo && (appearance.previousImageUrl || appearance.previousImageUrls.length > 0) && ( -
+ {/* Body */} +
t(key as never)} + t={t} shotType={shotType} cameraMove={cameraMove} description={description} @@ -109,6 +135,8 @@ export default function AIDataModal({ videoPrompt={videoPrompt} photographyRules={photographyRules} actingNotes={actingNotes} + activeCharIdx={activeCharIdx} + onActiveCharIdxChange={setActiveCharIdx} onShotTypeChange={setShotType} onCameraMoveChange={setCameraMove} onDescriptionChange={setDescription} @@ -117,29 +145,34 @@ export default function AIDataModal({ onPhotographyCharacterChange={updatePhotographyCharacter} onActingCharacterChange={updateActingCharacter} /> - t(key as never)} + t={t} previewJson={previewJson} />
-
- - + {/* Footer */} +
+

+ {characters.map(c => c.name).join('、')} + {location ? ` · ${location}` : ''} +

+
+ + {t('common.cancel')} + + } + > + {t('aiData.save')} + +
-
+ , + document.body ) } diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModal.types.ts b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModal.types.ts index 71f7ecc..d09aaa3 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModal.types.ts +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModal.types.ts @@ -1,5 +1,11 @@ 'use client' +export interface AIDataCharacter { + name: string + appearance: string + slot?: string +} + export interface PhotographyCharacter { name: string screen_position: string @@ -47,7 +53,7 @@ export interface AIDataModalProps { cameraMove: string | null description: string | null location: string | null - characters: string[] + characters: AIDataCharacter[] videoPrompt: string | null photographyRules: PhotographyRules | null actingNotes: ActingNotes | ActingCharacter[] | null diff --git a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModalFormPane.tsx b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModalFormPane.tsx index 865e948..734151f 100644 --- a/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModalFormPane.tsx +++ b/src/app/[locale]/workspace/[projectId]/modes/novel-promotion/components/storyboard/AIDataModalFormPane.tsx @@ -1,21 +1,29 @@ 'use client' +import { useEffect, useRef, useState, type ChangeEvent } from 'react' +import type { useTranslations } from 'next-intl' +import GlassInput from '@/components/ui/primitives/GlassInput' +import GlassTextarea from '@/components/ui/primitives/GlassTextarea' +import { AppIcon } from '@/components/ui/icons' import type { ActingCharacter, + AIDataCharacter, PhotographyCharacter, PhotographyRules, } from './AIDataModal.types' interface AIDataModalFormPaneProps { - t: (key: string) => string + t: ReturnType> shotType: string cameraMove: string description: string location: string | null - characters: string[] + characters: AIDataCharacter[] videoPrompt: string photographyRules: PhotographyRules | null actingNotes: ActingCharacter[] + activeCharIdx: number + onActiveCharIdxChange: (idx: number) => void onShotTypeChange: (value: string) => void onCameraMoveChange: (value: string) => void onDescriptionChange: (value: string) => void @@ -25,6 +33,101 @@ interface AIDataModalFormPaneProps { onActingCharacterChange: (index: number, field: keyof ActingCharacter, value: string) => void } +function FL({ children }: { children: string }) { + return

{children}

+} + +function AutoGrowTextarea({ + value, + onChange, + rows, + placeholder, + density = 'default', + className, +}: { + value: string + onChange: (event: ChangeEvent) => void + rows: number + placeholder?: string + density?: 'compact' | 'default' + className?: string +}) { + const ref = useRef(null) + + useEffect(() => { + const el = ref.current + if (!el) return + el.style.height = '0px' + el.style.height = `${el.scrollHeight}px` + }, [value]) + + return ( + { + const el = event.currentTarget + el.style.height = '0px' + el.style.height = `${el.scrollHeight}px` + }} + placeholder={placeholder} + density={density} + className={['overflow-hidden', className].filter(Boolean).join(' ')} + /> + ) +} + +function SectionLabel({ children }: { children: string }) { + return ( +
+ + {children} +
+ ) +} + +function CollapseSection({ + label, + iconName, + children, +}: { + label: string + iconName?: 'video' | 'film' + children: React.ReactNode +}) { + const [open, setOpen] = useState(false) + return ( +
+ + {open && ( +
+ {children} +
+ )} +
+ ) +} + export default function AIDataModalFormPane({ t, shotType, @@ -35,6 +138,8 @@ export default function AIDataModalFormPane({ videoPrompt, photographyRules, actingNotes, + activeCharIdx, + onActiveCharIdxChange, onShotTypeChange, onCameraMoveChange, onDescriptionChange, @@ -43,194 +148,257 @@ export default function AIDataModalFormPane({ onPhotographyCharacterChange, onActingCharacterChange, }: AIDataModalFormPaneProps) { + const activeChar = characters[activeCharIdx] + const photoChar = photographyRules?.characters.find(c => c.name === activeChar?.name) + const actingCharIdx = actingNotes.findIndex(n => n.name === activeChar?.name) + const actingChar = actingCharIdx >= 0 ? actingNotes[actingCharIdx] : null + return ( -
-
{t('aiData.basicData')}
+
-
-
- - onShotTypeChange(event.target.value)} - className="w-full px-3 py-2 border border-[var(--glass-stroke-strong)] rounded-lg text-sm focus:ring-2 focus:ring-[var(--glass-tone-info-fg)] focus:border-[var(--glass-stroke-focus)]" - placeholder={t('aiData.shotTypePlaceholder')} - /> + {/* ① 视觉描述 — 最高优先 */} +
+
+ + + {t('aiData.visualDescription')} +
-
- - onCameraMoveChange(event.target.value)} - className="w-full px-3 py-2 border border-[var(--glass-stroke-strong)] rounded-lg text-sm focus:ring-2 focus:ring-[var(--glass-tone-info-fg)] focus:border-[var(--glass-stroke-focus)]" - placeholder={t('aiData.cameraMovePlaceholder')} - /> -
-
- -
-
- -
- {location || t('aiData.notSelected')} -
-
-
- -
- {characters.length > 0 ? characters.join('、') : t('common.none')} -
-
-
- -
- -