@oh-story/dsh 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -12
- package/lib/client.js +2692 -268
- package/lib/client.js.map +4 -4
- package/lib/drama/manifest.json +136 -101
- package/lib/drama/skills/short-drama/SKILL.md +61 -1
- package/lib/drama/skills/short-drama/references/contract-and-ownership.md +7 -4
- package/lib/drama/skills/short-drama/references/creator-documents.md +133 -8
- package/lib/drama/skills/short-drama/references/creator-workflow.md +38 -2
- package/lib/drama/skills/short-drama/references/knowhow-index.md +3 -0
- package/lib/drama/skills/short-drama/references/reference-roles.md +10 -7
- package/lib/drama/skills/short-drama/references/runtime-preflight.md +3 -0
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +1002 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +485 -5
- package/lib/drama/skills/short-drama-assets/SKILL.md +13 -3
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +116 -0
- package/lib/drama/skills/short-drama-assets/references/identity-vs-variant.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/prop-and-state.md +19 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-assets/scripts/asset_check.py +1 -1
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +6 -2
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +4 -3
- package/lib/drama/skills/short-drama-image-prompts/references/lookdev-frame.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-image-prompts/references/stage-contract.md +3 -1
- package/lib/drama/skills/short-drama-image-prompts/scripts/image_prompt_check.py +8 -8
- package/lib/drama/skills/short-drama-image-prompts/scripts/selftest.py +4 -2
- package/lib/drama/skills/short-drama-produce/SKILL.md +46 -10
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +65 -8
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +85 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +27 -10
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +362 -3
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +627 -40
- package/lib/drama/skills/short-drama-produce/scripts/selftest.py +5 -4
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +12 -0
- package/lib/drama/skills/short-drama-review/scripts/review_check.py +1 -1
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +76 -7
- package/lib/drama/skills/short-drama-storyboard/assets/shot-template.jsonl +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/keyframe-craft.md +6 -2
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +19 -4
- package/lib/drama/skills/short-drama-storyboard/references/review-and-fixtures.md +5 -2
- package/lib/drama/skills/short-drama-storyboard/references/screenplay-to-keyframe-example.md +16 -0
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +8 -3
- package/lib/drama/skills/short-drama-storyboard/scripts/storyboard_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +81 -13
- package/lib/drama/skills/short-drama-video-prompts/assets/motion-spec.jsonl.md +6 -1
- package/lib/drama/skills/short-drama-video-prompts/assets/video-prompts.md +17 -1
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +21 -1
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +154 -0
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -6
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +11 -7
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +41 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +66 -0
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +5 -2
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +115 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/container_check.py +1 -1
- package/lib/drama/skills/short-drama-video-prompts/scripts/music_spec_check.py +1 -1
- package/lib/drama/skills/short-drama-write/SKILL.md +3 -2
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +1 -1
- package/lib/drama/skills/short-drama-write/scripts/screenplay_index.py +2 -2
- package/lib/drama/skills/short-drama-write/scripts/voice_sheet_check.py +1 -1
- package/lib/index.js +865 -33
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/LICENSE +21 -0
- package/lib/novel-to-game/README.md +180 -0
- package/lib/novel-to-game/README_ZH.md +178 -0
- package/lib/novel-to-game/VERSION +1 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +217 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/.vercelignore +3 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +130 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +1105 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +2539 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +18 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +147 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +332 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +7472 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +13077 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +3028 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +32 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +10 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/vercel.json +23 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +161 -0
- package/lib/novel-to-game/examples/jin-ping-mei/example.json +12 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +31 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +99 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +41 -0
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +129 -0
- package/lib/novel-to-game/manifest.json +508 -0
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +73 -0
- package/lib/novel-to-game/skills/game-art-direction/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +119 -0
- package/lib/novel-to-game/skills/game-build/SKILL.md +84 -0
- package/lib/novel-to-game/skills/game-build/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +109 -0
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +56 -0
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +81 -0
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +37 -0
- package/lib/novel-to-game/skills/game-concept/SKILL.md +72 -0
- package/lib/novel-to-game/skills/game-concept/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +94 -0
- package/lib/novel-to-game/skills/game-qa/SKILL.md +50 -0
- package/lib/novel-to-game/skills/game-qa/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-qa/references/qa-contract.md +66 -0
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +45 -0
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +63 -0
- package/lib/novel-to-game/skills/game-world-design/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +59 -0
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +53 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +84 -0
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +56 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +124 -0
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +77 -0
- package/lib/novel-to-game/skills/novel-game-analyze/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +71 -0
- package/lib/novel-to-game/skills/novel-to-game/agents/openai.yaml +4 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +42 -0
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +51 -0
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +65 -0
- package/lib/oh-story/manifest.json +415 -305
- package/lib/oh-story/roles/consistency-checker.md +1 -1
- package/lib/oh-story/roles/narrative-writer.md +32 -33
- package/lib/oh-story/roles/story-architect.md +16 -26
- package/lib/oh-story/skills/browser-cdp/SKILL.md +1 -1
- package/lib/oh-story/skills/story/SKILL.md +16 -9
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-cover/SKILL.md +1 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +10 -5
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-import/SKILL.md +19 -10
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +2 -2
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +4 -4
- package/lib/oh-story/skills/story-long-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +35 -21
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +7 -3
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +6 -6
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-setup/references/agent-references/hooks-chapter.md → story-long-write/references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-long-write/references/{quality-checklist.md → long-chapter-quality.md} +2 -1
- package/lib/oh-story/skills/story-long-write/references/long-format.md +42 -0
- package/lib/oh-story/skills/{story-setup/references/agent-references/genre-catalog.md → story-long-write/references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/{story-setup/references/agent-references/genre-core-mechanics.md → story-long-write/references/long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/{story-short-write/references/reversal-toolkit.md → story-long-write/references/long-reversal.md} +18 -32
- package/lib/oh-story/skills/story-long-write/references/{hooks-suspense.md → long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +32 -27
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +14 -14
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +54 -54
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +251 -0
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +303 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-review/SKILL.md +26 -17
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-review/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-review/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-review/references/{quality-checklist.md → review-quality.md} +22 -21
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +14 -6
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +98 -9
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +246 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +74 -33
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +72 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +2 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +9 -10
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-readers.md +1 -1
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-chapter.md → story-setup/references/agent-references/long-chapter-hooks.md} +5 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
- package/lib/oh-story/skills/{story-long-write/references/genre-catalog.md → story-setup/references/agent-references/long-genre-catalog.md} +11 -97
- package/lib/oh-story/skills/{story-long-write/references/genre-core-mechanics.md → story-setup/references/agent-references/long-genre-mechanics.md} +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +40 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/{reversal-toolkit.md → long-reversal.md} +18 -32
- package/lib/oh-story/skills/{story-short-write/references/hooks-suspense.md → story-setup/references/agent-references/long-suspense.md} +6 -6
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/{emotional-methods.md → short-emotional-methods.md} +4 -4
- package/lib/oh-story/skills/{story-long-write/references/genre-writing-formulas.md → story-setup/references/agent-references/short-genre-formulas.md} +32 -44
- package/lib/oh-story/skills/story-setup/references/agent-references/short-quality.md +39 -0
- package/lib/oh-story/skills/{story-long-write/references/reversal-toolkit.md → story-setup/references/agent-references/short-reversal.md} +29 -23
- package/lib/oh-story/skills/story-setup/references/agent-references/{hooks-suspense.md → short-suspense.md} +31 -31
- package/lib/oh-story/skills/story-setup/references/agent-references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +55 -55
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +28 -28
- package/lib/oh-story/skills/story-short-analyze/references/{character-basics.md → analysis-character-basics.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-design-methods.md → analysis-character-design.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{character-relations.md → analysis-character-relations.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{hooks-paragraph.md → analysis-paragraph-hooks.md} +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/{genre-readers.md → analysis-reader-profile.md} +2 -2
- package/lib/oh-story/skills/story-short-analyze/references/analysis-report-style.md +45 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
- package/lib/oh-story/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
- package/lib/oh-story/skills/story-short-analyze/references/{genre-writing-techniques.md → analysis-writing-techniques.md} +21 -19
- package/lib/oh-story/skills/story-short-analyze/references/deconstruction-examples.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +21 -28
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +17 -17
- package/lib/oh-story/skills/story-short-analyze/references/{quality-checklist.md → source-story-quality.md} +28 -49
- package/lib/oh-story/skills/story-short-analyze/references/zhihu-style.md +2 -2
- package/lib/oh-story/skills/story-short-scan/SKILL.md +1 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +78 -55
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +163 -0
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +6 -4
- package/lib/oh-story/skills/story-short-write/references/cross-book-recall.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +6 -6
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +7 -7
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +32 -44
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +20 -18
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +18 -18
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/short-format.md +38 -8
- package/lib/oh-story/skills/story-short-write/references/{quality-checklist.md → short-prose-quality.md} +22 -21
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +355 -0
- package/lib/oh-story/skills/{story-short-analyze/references/hooks-suspense.md → story-short-write/references/short-suspense.md} +31 -31
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +31 -22
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +871 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +64 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +212 -0
- package/lib/oh-story/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
- package/lib/types/client/drama-production-view.d.ts +34 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -0
- package/lib/types/client/drama-production.d.ts +92 -0
- package/lib/types/client/drama-production.d.ts.map +1 -0
- package/lib/types/client/file-activity.d.ts +4 -2
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-intents.d.ts +9 -0
- package/lib/types/client/production-intents.d.ts.map +1 -0
- package/lib/types/client/production-prompts.d.ts +9 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -0
- package/lib/types/client/production-runtime.d.ts +58 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -0
- package/lib/types/client/video-studio.d.ts +41 -0
- package/lib/types/client/video-studio.d.ts.map +1 -0
- package/lib/types/client/workbench-ui.d.ts +5 -0
- package/lib/types/client/workbench-ui.d.ts.map +1 -0
- package/lib/types/game-verification.d.ts +16 -0
- package/lib/types/game-verification.d.ts.map +1 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/production-intent.d.ts +21 -0
- package/lib/types/production-intent.d.ts.map +1 -0
- package/lib/types/production-tool.d.ts +5 -0
- package/lib/types/production-tool.d.ts.map +1 -0
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts +7 -0
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts +42 -0
- package/lib/types/video-project.d.ts.map +1 -0
- package/lib/types/workspace-request-trust.d.ts +6 -0
- package/lib/types/workspace-request-trust.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts +20 -0
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +282 -0
- package/lib/video-recap/LICENSE +21 -0
- package/lib/video-recap/README.en.md +257 -0
- package/lib/video-recap/README.md +255 -0
- package/lib/video-recap/manifest.json +684 -0
- package/lib/video-recap/skills/video-assemble/SKILL.md +97 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +15 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +345 -0
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +34 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +305 -0
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +94 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +205 -0
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +555 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +261 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +767 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +111 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +24 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_templates.py +37 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +191 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +75 -0
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +394 -0
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +239 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +243 -0
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +477 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +46 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +486 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +376 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +88 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +248 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +89 -0
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +512 -0
- package/lib/video-recap/skills/video-cut/SKILL.md +90 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +67 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +320 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +545 -0
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +348 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +77 -0
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +341 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +745 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +216 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +61 -0
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-recap/references/data-schema.md +534 -0
- package/lib/video-recap/skills/video-recap/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +23 -0
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +251 -0
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +483 -0
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +737 -0
- package/lib/video-recap/skills/video-recap/scripts/lib.py +242 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +457 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +95 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +18 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_contract.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +371 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +201 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +227 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +362 -0
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +129 -0
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +338 -0
- package/lib/video-recap/skills/video-recap/scripts/recap.py +9 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +140 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +550 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +77 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +654 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +335 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +217 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +795 -0
- package/lib/video-recap/skills/video-script/SKILL.md +248 -0
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +225 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +75 -0
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +492 -0
- package/lib/video-recap/skills/video-script/scripts/lib.py +327 -0
- package/lib/video-recap/skills/video-script/scripts/narration.py +12 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-script/scripts/review.py +42 -0
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +262 -0
- package/lib/video-recap/skills/video-script/scripts/review_response.py +761 -0
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +200 -0
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-script/scripts/validate.py +164 -0
- package/lib/video-recap/skills/video-understanding/SKILL.md +75 -0
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +299 -0
- package/lib/video-recap/skills/video-understanding/references/prompt-templates.md +37 -0
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +74 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +399 -0
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +410 -0
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +260 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +12 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +403 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +332 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +442 -0
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +647 -0
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +237 -0
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +513 -0
- package/lib/video-recap/skills/video-understanding/scripts/extract.py +62 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +470 -0
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +678 -0
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +204 -0
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +507 -0
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +199 -0
- package/lib/video-recap/skills/video-understanding/scripts/understand.py +9 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +216 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +327 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +367 -0
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +162 -0
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +705 -0
- package/lib/video-recap/skills/video-voiceover/SKILL.md +81 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +637 -0
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +109 -0
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +421 -0
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +113 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +782 -0
- package/package.json +55 -40
- package/lib/oh-story/skills/story-deslop/scripts/check-outline-copy.js +0 -241
- package/lib/oh-story/skills/story-long-write/references/format-and-structure.md +0 -169
- package/lib/oh-story/skills/story-setup/references/agent-references/genre-writing-formulas.md +0 -444
- package/lib/oh-story/skills/story-setup/references/agent-references/quality-checklist.md +0 -247
- package/lib/oh-story/skills/story-short-analyze/references/anti-ai-writing.md +0 -582
- package/lib/oh-story/skills/story-short-analyze/references/banned-words.md +0 -147
- package/lib/oh-story/skills/story-short-analyze/references/genre-catalog.md +0 -490
- package/lib/oh-story/skills/story-short-analyze/references/genre-core-mechanics.md +0 -425
- package/lib/oh-story/skills/story-short-analyze/references/genre-writing-formulas.md +0 -453
- /package/lib/oh-story/skills/{story-long-write/references/hooks-chapter.md → story-setup/references/agent-references/short-chapter-hooks.md} +0 -0
- /package/lib/oh-story/skills/{story-long-write/references/hooks-paragraph.md → story-setup/references/agent-references/short-paragraph-hooks.md} +0 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Storyboard (contact-sheet) generation for video-understanding.
|
|
3
|
+
|
|
4
|
+
Two ADVISORY artifacts that help the writing agent orient on the timeline by SCANNING
|
|
5
|
+
one image instead of opening dozens of frames:
|
|
6
|
+
|
|
7
|
+
- source_storyboard.{jpg,json} — scene-anchored tiles over the SOURCE timeline.
|
|
8
|
+
- edited_storyboard.{jpg,json} — one row per kept clip over the cut OUTPUT timeline,
|
|
9
|
+
each tile dual-labelled (output time / source time).
|
|
10
|
+
|
|
11
|
+
Both reuse the frames already extracted by understand.py (frames/frame_*.jpg at CONFIG["fps"]).
|
|
12
|
+
Nothing here re-extracts video. Every function returns dict|None and degrades to
|
|
13
|
+
None + log(...) on ANY failure (no frames, ffmpeg missing/non-zero, font probe raises),
|
|
14
|
+
so a storyboard quirk can NEVER block the pipeline (Principle 1: advisory, never blocking).
|
|
15
|
+
|
|
16
|
+
drawtext "mm:ss" labels are attempted when a usable font is found (labels_burned:true);
|
|
17
|
+
when no font is available (or drawtext errors) the sheet is still produced UNLABELLED
|
|
18
|
+
(labels_burned:false) and the JSON sidecar stays authoritative for all timestamps.
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import math
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from extract import frame_number_for_time, parse_frame_number
|
|
27
|
+
from lib import CONFIG, run_cmd, log
|
|
28
|
+
|
|
29
|
+
try: # file_fingerprint is the project's content-fingerprint helper; reuse when cheap.
|
|
30
|
+
from lib import file_fingerprint
|
|
31
|
+
except ImportError: # pragma: no cover - lib always ships it; degrade gracefully if not.
|
|
32
|
+
file_fingerprint = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Candidate font files probed (in order) for burning mm:ss labels. The first that exists
|
|
36
|
+
# AND that drawtext can actually load wins. A probe that RAISES must never abort the sheet.
|
|
37
|
+
_FONT_CANDIDATES = (
|
|
38
|
+
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
|
39
|
+
"/System/Library/Fonts/Helvetica.ttc",
|
|
40
|
+
"/Library/Fonts/Arial.ttf",
|
|
41
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
42
|
+
"/usr/share/fonts/dejavu/DejaVuSans.ttf",
|
|
43
|
+
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
|
44
|
+
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _fmt_mmss(seconds):
|
|
49
|
+
"""Format a timestamp as mm:ss (clamped to >= 0)."""
|
|
50
|
+
total = int(round(max(0.0, float(seconds))))
|
|
51
|
+
return f"{total // 60:02d}:{total % 60:02d}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _ffmpeg_available():
|
|
55
|
+
return shutil.which("ffmpeg") is not None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _probe_font():
|
|
59
|
+
"""Return a usable font file path, or None. Any exception → None (never raises out).
|
|
60
|
+
|
|
61
|
+
Probe order: explicit candidate files first, then `fc-match` if available. The path
|
|
62
|
+
is only returned when the file exists on disk; we do NOT shell out to drawtext here —
|
|
63
|
+
a render-time drawtext error is caught separately and also degrades to unlabelled.
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
for candidate in _FONT_CANDIDATES:
|
|
67
|
+
if Path(candidate).is_file():
|
|
68
|
+
return candidate
|
|
69
|
+
fc_match = shutil.which("fc-match")
|
|
70
|
+
if fc_match:
|
|
71
|
+
result = subprocess.run(
|
|
72
|
+
[fc_match, "-f", "%{file}", "sans"],
|
|
73
|
+
capture_output=True, text=True, timeout=10,
|
|
74
|
+
)
|
|
75
|
+
path = (result.stdout or "").strip()
|
|
76
|
+
if result.returncode == 0 and path and Path(path).is_file():
|
|
77
|
+
return path
|
|
78
|
+
except Exception as exc: # noqa: BLE001 - a font probe must NEVER abort the sheet
|
|
79
|
+
log(f"storyboard 字体探测异常(降级为不烧时间戳): {exc}")
|
|
80
|
+
return None
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _frame_index(work_dir):
|
|
85
|
+
"""Return (sorted_frame_paths, sorted_numbers) for frames/frame_*.jpg, or ([], [])."""
|
|
86
|
+
frames_dir = Path(work_dir) / "frames"
|
|
87
|
+
if not frames_dir.is_dir():
|
|
88
|
+
return [], []
|
|
89
|
+
pairs = []
|
|
90
|
+
for path in frames_dir.glob("frame_*.jpg"):
|
|
91
|
+
number = parse_frame_number(path)
|
|
92
|
+
if number is None:
|
|
93
|
+
continue
|
|
94
|
+
pairs.append((number, path))
|
|
95
|
+
pairs.sort(key=lambda item: item[0])
|
|
96
|
+
numbers = [num for num, _ in pairs]
|
|
97
|
+
paths = [path for _, path in pairs]
|
|
98
|
+
return paths, numbers
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _nearest_existing_frame(timestamp, fps, paths, numbers):
|
|
102
|
+
"""Map a SOURCE timestamp → the nearest EXISTING frame file, clamped to [first,last].
|
|
103
|
+
|
|
104
|
+
Frames are named frame_{n:05d}.jpg; the number↔time mapping is owned by extract.py
|
|
105
|
+
(frame_00001 is t=0, so n = t*fps + 1). Rounding a timestamp blindly can yield a frame
|
|
106
|
+
number that was never written (fps boundary / last frame gap), so we resolve to the
|
|
107
|
+
closest number that actually exists on disk.
|
|
108
|
+
"""
|
|
109
|
+
if not numbers:
|
|
110
|
+
return None
|
|
111
|
+
fps = float(fps)
|
|
112
|
+
if fps <= 0:
|
|
113
|
+
return None
|
|
114
|
+
target = frame_number_for_time(timestamp, fps)
|
|
115
|
+
# Clamp into the real extracted range so out-of-range timestamps pin to first/last frame.
|
|
116
|
+
if target <= numbers[0]:
|
|
117
|
+
return paths[0]
|
|
118
|
+
if target >= numbers[-1]:
|
|
119
|
+
return paths[-1]
|
|
120
|
+
# numbers is sorted; find the closest by absolute distance (ties → earlier frame).
|
|
121
|
+
best_idx = 0
|
|
122
|
+
best_dist = None
|
|
123
|
+
for idx, num in enumerate(numbers):
|
|
124
|
+
dist = abs(num - target)
|
|
125
|
+
if best_dist is None or dist < best_dist:
|
|
126
|
+
best_dist = dist
|
|
127
|
+
best_idx = idx
|
|
128
|
+
elif num - target > best_dist:
|
|
129
|
+
break # sorted: distance only grows from here
|
|
130
|
+
return paths[best_idx]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _scene_anchor_timestamps(scenes, max_tiles):
|
|
134
|
+
"""Scene-anchored sample timestamps: each scene midpoint; long scenes also +1/3 & +2/3.
|
|
135
|
+
|
|
136
|
+
Returns a list of (scene_id, timestamp). A scene is "long" when adding the thirds gives
|
|
137
|
+
materially distinct sample points; we treat scenes longer than `long_scene_seconds` as
|
|
138
|
+
long. Total is capped at max_tiles by evenly subsampling the ordered anchor list, so the
|
|
139
|
+
sheet stays legible (D2: one bounded contact sheet, not dozens of frame reads).
|
|
140
|
+
"""
|
|
141
|
+
long_scene_seconds = float(CONFIG.get("storyboard_long_scene_seconds", 6.0) or 6.0)
|
|
142
|
+
anchors = []
|
|
143
|
+
for scene_id, scene in enumerate(scenes or []):
|
|
144
|
+
try:
|
|
145
|
+
start = float(scene["start"])
|
|
146
|
+
end = float(scene["end"])
|
|
147
|
+
except (KeyError, TypeError, ValueError):
|
|
148
|
+
continue
|
|
149
|
+
if end <= start:
|
|
150
|
+
continue
|
|
151
|
+
mid = (start + end) / 2.0
|
|
152
|
+
if (end - start) >= long_scene_seconds:
|
|
153
|
+
third = start + (end - start) / 3.0
|
|
154
|
+
two_third = start + 2.0 * (end - start) / 3.0
|
|
155
|
+
points = [third, mid, two_third]
|
|
156
|
+
else:
|
|
157
|
+
points = [mid]
|
|
158
|
+
for ts in points:
|
|
159
|
+
anchors.append((scene_id, round(ts, 3)))
|
|
160
|
+
if max_tiles and len(anchors) > max_tiles:
|
|
161
|
+
# Evenly subsample to the cap, preserving timeline order and scene spread.
|
|
162
|
+
step = len(anchors) / float(max_tiles)
|
|
163
|
+
anchors = [anchors[int(i * step)] for i in range(max_tiles)]
|
|
164
|
+
return anchors
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _file_fp(path):
|
|
168
|
+
if file_fingerprint is None:
|
|
169
|
+
return None
|
|
170
|
+
try:
|
|
171
|
+
return file_fingerprint(path)
|
|
172
|
+
except (OSError, ValueError):
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _labelled_frame(frame_path, label, font_path, scratch_dir, out_name):
|
|
177
|
+
"""Burn `label` onto a copy of frame_path via drawtext; return the labelled path or None.
|
|
178
|
+
|
|
179
|
+
None signals the caller to fall back to the original frame (and flip labels_burned off).
|
|
180
|
+
A drawtext failure here is non-fatal: the unlabelled frame still tiles fine.
|
|
181
|
+
"""
|
|
182
|
+
out_path = scratch_dir / out_name
|
|
183
|
+
safe_label = label.replace("\\", "\\\\").replace(":", "\\:").replace("'", "’")
|
|
184
|
+
drawtext = (
|
|
185
|
+
f"drawtext=fontfile='{font_path}':text='{safe_label}':"
|
|
186
|
+
"x=8:y=8:fontsize=28:fontcolor=white:"
|
|
187
|
+
"box=1:boxcolor=black@0.55:boxborderw=6"
|
|
188
|
+
)
|
|
189
|
+
cmd = ["ffmpeg", "-y", "-i", str(frame_path), "-vf", drawtext, "-frames:v", "1", str(out_path)]
|
|
190
|
+
try:
|
|
191
|
+
result = run_cmd(cmd)
|
|
192
|
+
except Exception as exc: # noqa: BLE001 - a label render must never abort the sheet
|
|
193
|
+
log(f"storyboard drawtext 异常(降级为不烧时间戳): {exc}")
|
|
194
|
+
return None
|
|
195
|
+
if result.returncode != 0 or not out_path.exists():
|
|
196
|
+
return None
|
|
197
|
+
return out_path
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _tile_pages(frame_paths, columns, out_dir, out_stem, scratch_dir):
|
|
201
|
+
"""Tile frame_paths into one or more contact-sheet pages; return the list of page paths.
|
|
202
|
+
|
|
203
|
+
ffmpeg's tile filter lays one grid per page. We page so each sheet holds at most
|
|
204
|
+
columns*rows tiles where rows is chosen to keep the grid roughly square but capped, then
|
|
205
|
+
spill into _001.jpg, _002.jpg… Returns [] on any ffmpeg failure (caller degrades to None).
|
|
206
|
+
"""
|
|
207
|
+
columns = max(1, int(columns))
|
|
208
|
+
rows_per_page = max(1, int(CONFIG.get("storyboard_rows_per_page", 5) or 5))
|
|
209
|
+
per_page = columns * rows_per_page
|
|
210
|
+
pages = []
|
|
211
|
+
total = len(frame_paths)
|
|
212
|
+
page_count = max(1, math.ceil(total / per_page))
|
|
213
|
+
for page_idx in range(page_count):
|
|
214
|
+
chunk = frame_paths[page_idx * per_page:(page_idx + 1) * per_page]
|
|
215
|
+
if not chunk:
|
|
216
|
+
continue
|
|
217
|
+
cols = min(columns, len(chunk))
|
|
218
|
+
rows = max(1, math.ceil(len(chunk) / cols))
|
|
219
|
+
if page_count == 1:
|
|
220
|
+
page_path = out_dir / f"{out_stem}.jpg"
|
|
221
|
+
else:
|
|
222
|
+
page_path = out_dir / f"{out_stem}_{page_idx + 1:03d}.jpg"
|
|
223
|
+
# Stage the chunk as a contiguous numbered sequence so ffmpeg's image2 demuxer +
|
|
224
|
+
# tile filter consume EXACTLY these frames (the source frame numbers are sparse).
|
|
225
|
+
seq_dir = scratch_dir / f"seq_{out_stem}_{page_idx:03d}"
|
|
226
|
+
seq_dir.mkdir(parents=True, exist_ok=True)
|
|
227
|
+
for seq_idx, src in enumerate(chunk):
|
|
228
|
+
shutil.copyfile(src, seq_dir / f"f_{seq_idx:05d}.jpg")
|
|
229
|
+
cmd = [
|
|
230
|
+
"ffmpeg", "-y",
|
|
231
|
+
"-framerate", "1",
|
|
232
|
+
"-i", str(seq_dir / "f_%05d.jpg"),
|
|
233
|
+
"-frames:v", "1",
|
|
234
|
+
"-vf", f"tile={cols}x{rows}",
|
|
235
|
+
str(page_path),
|
|
236
|
+
]
|
|
237
|
+
try:
|
|
238
|
+
result = run_cmd(cmd)
|
|
239
|
+
except Exception as exc: # noqa: BLE001
|
|
240
|
+
log(f"storyboard tile 异常: {exc}")
|
|
241
|
+
return []
|
|
242
|
+
if result.returncode != 0 or not page_path.exists():
|
|
243
|
+
log(f"storyboard tile 失败: {getattr(result, 'stderr', '')[-300:]}")
|
|
244
|
+
return []
|
|
245
|
+
pages.append(page_path)
|
|
246
|
+
return pages
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _render_storyboard(work_dir, tiles, out_stem):
|
|
250
|
+
"""Shared render path: optionally burn labels, tile to pages, return (page_paths, labels_burned).
|
|
251
|
+
|
|
252
|
+
`tiles` is a list of dicts that ALREADY carry a resolved `frame_file` (absolute path) and a
|
|
253
|
+
`label` string. Returns (None, _) on hard failure so callers degrade to None.
|
|
254
|
+
"""
|
|
255
|
+
if not _ffmpeg_available():
|
|
256
|
+
log("storyboard 跳过:未找到 ffmpeg")
|
|
257
|
+
return None, False
|
|
258
|
+
storyboard_dir = Path(work_dir) / "storyboard"
|
|
259
|
+
storyboard_dir.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
scratch_dir = storyboard_dir / f".scratch_{out_stem}"
|
|
261
|
+
if scratch_dir.exists():
|
|
262
|
+
shutil.rmtree(scratch_dir, ignore_errors=True)
|
|
263
|
+
scratch_dir.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
|
|
265
|
+
font_path = _probe_font()
|
|
266
|
+
labels_burned = bool(font_path)
|
|
267
|
+
render_frames = []
|
|
268
|
+
if labels_burned:
|
|
269
|
+
for idx, tile in enumerate(tiles):
|
|
270
|
+
labelled = _labelled_frame(
|
|
271
|
+
Path(tile["frame_file"]), tile["label"], font_path, scratch_dir,
|
|
272
|
+
f"lbl_{out_stem}_{idx:05d}.jpg",
|
|
273
|
+
)
|
|
274
|
+
if labelled is None:
|
|
275
|
+
# First failure → abandon labelling entirely so the WHOLE sheet is consistent
|
|
276
|
+
# (no half-labelled pages). The JSON sidecar still carries every timestamp.
|
|
277
|
+
labels_burned = False
|
|
278
|
+
break
|
|
279
|
+
render_frames.append(labelled)
|
|
280
|
+
if not labels_burned:
|
|
281
|
+
render_frames = [Path(tile["frame_file"]) for tile in tiles]
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
pages = _tile_pages(
|
|
285
|
+
render_frames, CONFIG.get("storyboard_columns", 6),
|
|
286
|
+
storyboard_dir, out_stem, scratch_dir,
|
|
287
|
+
)
|
|
288
|
+
finally:
|
|
289
|
+
shutil.rmtree(scratch_dir, ignore_errors=True)
|
|
290
|
+
if not pages:
|
|
291
|
+
return None, labels_burned
|
|
292
|
+
return pages, labels_burned
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def build_source_storyboard(work_dir, video_path, scenes, fps):
|
|
296
|
+
"""Scene-anchored SOURCE-timeline contact sheet. Returns dict|None.
|
|
297
|
+
|
|
298
|
+
Samples each scene midpoint (long scenes also +1/3,+2/3), maps every sample to the
|
|
299
|
+
nearest EXISTING extracted frame (clamped), tiles them, and writes
|
|
300
|
+
storyboard/source_storyboard.json. Returns None + log on any failure.
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
work_dir = Path(work_dir)
|
|
304
|
+
paths, numbers = _frame_index(work_dir)
|
|
305
|
+
if not paths:
|
|
306
|
+
log("storyboard 跳过 source:frames/ 为空或缺失")
|
|
307
|
+
return None
|
|
308
|
+
max_tiles = int(CONFIG.get("storyboard_max_tiles", 30) or 30)
|
|
309
|
+
columns = int(CONFIG.get("storyboard_columns", 6) or 6)
|
|
310
|
+
anchors = _scene_anchor_timestamps(scenes, max_tiles)
|
|
311
|
+
if not anchors:
|
|
312
|
+
log("storyboard 跳过 source:无可用场景锚点")
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
tiles = []
|
|
316
|
+
for tile_id, (scene_id, ts) in enumerate(anchors):
|
|
317
|
+
frame = _nearest_existing_frame(ts, fps, paths, numbers)
|
|
318
|
+
if frame is None:
|
|
319
|
+
continue
|
|
320
|
+
tiles.append({
|
|
321
|
+
"tile_id": tile_id,
|
|
322
|
+
"timestamp": round(float(ts), 3),
|
|
323
|
+
"label": _fmt_mmss(ts),
|
|
324
|
+
"scene_id": scene_id,
|
|
325
|
+
"frame_file": str(frame),
|
|
326
|
+
})
|
|
327
|
+
if not tiles:
|
|
328
|
+
log("storyboard 跳过 source:未解析到任何帧")
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
pages, labels_burned = _render_storyboard(work_dir, tiles, "source_storyboard")
|
|
332
|
+
if not pages:
|
|
333
|
+
log("storyboard 跳过 source:拼贴失败")
|
|
334
|
+
return None
|
|
335
|
+
|
|
336
|
+
for tile in tiles:
|
|
337
|
+
tile["frame_file"] = Path(tile["frame_file"]).name
|
|
338
|
+
payload = {
|
|
339
|
+
"schema_version": 1,
|
|
340
|
+
"timeline": "source",
|
|
341
|
+
"video_path": str(video_path),
|
|
342
|
+
"video_fingerprint": _file_fp(video_path),
|
|
343
|
+
"fps": float(fps) if fps else None,
|
|
344
|
+
"labels_burned": labels_burned,
|
|
345
|
+
"page_images": [str(p) for p in pages],
|
|
346
|
+
"sample_policy": {
|
|
347
|
+
"max_tiles": max_tiles,
|
|
348
|
+
"columns": columns,
|
|
349
|
+
"anchors": "scene_midpoint+long_scene_thirds",
|
|
350
|
+
},
|
|
351
|
+
"tiles": tiles,
|
|
352
|
+
}
|
|
353
|
+
duration = get_video_duration_safe(video_path)
|
|
354
|
+
if duration:
|
|
355
|
+
payload["duration"] = round(duration, 3)
|
|
356
|
+
json_path = work_dir / "storyboard" / "source_storyboard.json"
|
|
357
|
+
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
358
|
+
log(f"storyboard source: {len(tiles)} tiles → {len(pages)} page(s), labels_burned={labels_burned}")
|
|
359
|
+
return payload
|
|
360
|
+
except Exception as exc: # noqa: BLE001 - advisory: never propagate
|
|
361
|
+
log(f"storyboard source 失败(忽略): {exc}")
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def get_video_duration_safe(video_path):
|
|
366
|
+
"""Best-effort source duration via ffprobe; returns None on any failure (advisory)."""
|
|
367
|
+
if not shutil.which("ffprobe"):
|
|
368
|
+
return None
|
|
369
|
+
try:
|
|
370
|
+
result = subprocess.run(
|
|
371
|
+
["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
|
|
372
|
+
"-of", "csv=p=0", str(video_path)],
|
|
373
|
+
capture_output=True, text=True, timeout=30,
|
|
374
|
+
)
|
|
375
|
+
if result.returncode != 0:
|
|
376
|
+
return None
|
|
377
|
+
return float((result.stdout or "").strip())
|
|
378
|
+
except (ValueError, OSError, subprocess.SubprocessError):
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _clip_plan_clips(clip_plan_validated):
|
|
383
|
+
"""Return the clips list from a validated plan (list or {"clips":[...]})."""
|
|
384
|
+
if isinstance(clip_plan_validated, dict):
|
|
385
|
+
clips = clip_plan_validated.get("clips", [])
|
|
386
|
+
else:
|
|
387
|
+
clips = clip_plan_validated
|
|
388
|
+
return clips if isinstance(clips, list) else []
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _source_to_output(source_time, clip):
|
|
392
|
+
"""Forward affine source→output map for ONE clip (reimplements cut.py:319 locally).
|
|
393
|
+
|
|
394
|
+
output = clip.output_start + (src − clip.source_start), clamped to [output_start, output_end].
|
|
395
|
+
Read the authoritative numbers from clip_plan_validated.json; do NOT import cut.py.
|
|
396
|
+
"""
|
|
397
|
+
src = float(source_time)
|
|
398
|
+
out_start = float(clip["output_start"])
|
|
399
|
+
out_end = float(clip["output_end"])
|
|
400
|
+
mapped = out_start + (src - float(clip["source_start"]))
|
|
401
|
+
return round(max(out_start, min(mapped, out_end)), 3)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def build_edited_storyboard(work_dir, source_video_path, clip_plan_validated, fps):
|
|
405
|
+
"""OUTPUT-timeline contact sheet: one row per kept clip over the cut. Returns dict|None.
|
|
406
|
+
|
|
407
|
+
For each kept clip, samples source start / mid / (end − 0.5s), maps each to the nearest
|
|
408
|
+
EXISTING SOURCE frame (SOURCE fps — frames are reused, NO re-extraction), and FRAME-IDENTITY
|
|
409
|
+
dedupes so a ≤1s clip yields 1-2 tiles (not 3 identical). Each tile is dual-labelled with
|
|
410
|
+
both `output_timestamp` and `source_timestamp` (+ `source_clip_id`). Writes
|
|
411
|
+
storyboard/edited_storyboard.json. Returns None + log on any failure.
|
|
412
|
+
"""
|
|
413
|
+
try:
|
|
414
|
+
work_dir = Path(work_dir)
|
|
415
|
+
paths, numbers = _frame_index(work_dir)
|
|
416
|
+
if not paths:
|
|
417
|
+
log("storyboard 跳过 edited:frames/ 为空或缺失")
|
|
418
|
+
return None
|
|
419
|
+
clips = _clip_plan_clips(clip_plan_validated)
|
|
420
|
+
if not clips:
|
|
421
|
+
log("storyboard 跳过 edited:clip_plan_validated 无 clips")
|
|
422
|
+
return None
|
|
423
|
+
columns = int(CONFIG.get("storyboard_columns", 6) or 6)
|
|
424
|
+
max_tiles = int(CONFIG.get("storyboard_max_tiles", 30) or 30)
|
|
425
|
+
|
|
426
|
+
tiles = []
|
|
427
|
+
seen_frames = set() # frame-identity dedupe (NOT luma de-dupe; that stays deferred)
|
|
428
|
+
tile_id = 0
|
|
429
|
+
for clip in clips:
|
|
430
|
+
try:
|
|
431
|
+
source_start = float(clip["source_start"])
|
|
432
|
+
source_end = float(clip["source_end"])
|
|
433
|
+
clip_id = clip.get("clip_id")
|
|
434
|
+
except (KeyError, TypeError, ValueError):
|
|
435
|
+
continue
|
|
436
|
+
if source_end <= source_start:
|
|
437
|
+
continue
|
|
438
|
+
mid = (source_start + source_end) / 2.0
|
|
439
|
+
end_sample = max(source_start, source_end - 0.5)
|
|
440
|
+
for src_ts in (source_start, mid, end_sample):
|
|
441
|
+
frame = _nearest_existing_frame(src_ts, fps, paths, numbers)
|
|
442
|
+
if frame is None:
|
|
443
|
+
continue
|
|
444
|
+
key = (clip_id, str(frame))
|
|
445
|
+
if key in seen_frames:
|
|
446
|
+
continue # same clip resolving to the same frame → drop the duplicate tile
|
|
447
|
+
seen_frames.add(key)
|
|
448
|
+
out_ts = _source_to_output(src_ts, clip)
|
|
449
|
+
tiles.append({
|
|
450
|
+
"tile_id": tile_id,
|
|
451
|
+
"output_timestamp": out_ts,
|
|
452
|
+
"source_timestamp": round(float(src_ts), 3),
|
|
453
|
+
"source_clip_id": clip_id,
|
|
454
|
+
"label": f"out {_fmt_mmss(out_ts)} / src {_fmt_mmss(src_ts)}",
|
|
455
|
+
"frame_file": str(frame),
|
|
456
|
+
})
|
|
457
|
+
tile_id += 1
|
|
458
|
+
if not tiles:
|
|
459
|
+
log("storyboard 跳过 edited:未解析到任何帧")
|
|
460
|
+
return None
|
|
461
|
+
if len(tiles) > max_tiles:
|
|
462
|
+
tiles = tiles[:max_tiles]
|
|
463
|
+
|
|
464
|
+
pages, labels_burned = _render_storyboard(work_dir, tiles, "edited_storyboard")
|
|
465
|
+
if not pages:
|
|
466
|
+
log("storyboard 跳过 edited:拼贴失败")
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
for tile in tiles:
|
|
470
|
+
tile["frame_file"] = Path(tile["frame_file"]).name
|
|
471
|
+
edited_source = Path(work_dir) / "edited_source.mp4"
|
|
472
|
+
payload = {
|
|
473
|
+
"schema_version": 1,
|
|
474
|
+
"timeline": "output",
|
|
475
|
+
"source_video_path": str(source_video_path),
|
|
476
|
+
"edited_video_path": str(edited_source) if edited_source.exists() else None,
|
|
477
|
+
"clip_plan_fingerprint": _clip_plan_fingerprint(clip_plan_validated),
|
|
478
|
+
"labels_burned": labels_burned,
|
|
479
|
+
"page_images": [str(p) for p in pages],
|
|
480
|
+
"sample_policy": {
|
|
481
|
+
"max_tiles": max_tiles,
|
|
482
|
+
"columns": columns,
|
|
483
|
+
"per_clip": "source_start+mid+(end-0.5s), frame-identity deduped",
|
|
484
|
+
},
|
|
485
|
+
"tiles": tiles,
|
|
486
|
+
}
|
|
487
|
+
json_path = work_dir / "storyboard" / "edited_storyboard.json"
|
|
488
|
+
json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
489
|
+
log(f"storyboard edited: {len(tiles)} tiles → {len(pages)} page(s), labels_burned={labels_burned}")
|
|
490
|
+
return payload
|
|
491
|
+
except Exception as exc: # noqa: BLE001 - advisory: never propagate
|
|
492
|
+
log(f"storyboard edited 失败(忽略): {exc}")
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _clip_plan_fingerprint(clip_plan_validated):
|
|
497
|
+
"""Stable fingerprint of the validated plan that drives the edited tiles."""
|
|
498
|
+
try:
|
|
499
|
+
return _stable_hash(clip_plan_validated)
|
|
500
|
+
except Exception: # noqa: BLE001
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _stable_hash(value):
|
|
505
|
+
import hashlib
|
|
506
|
+
blob = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
|
|
507
|
+
return hashlib.md5(blob.encode("utf-8")).hexdigest()
|