@oh-story/dsh 0.1.4 → 0.1.6
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 +2707 -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,227 @@
|
|
|
1
|
+
"""Build bounded semantic and multimodal MiMo QC payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from typing import Any, Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from mimo_qc_evidence import (
|
|
12
|
+
_cache_evidence,
|
|
13
|
+
_fingerprint_value,
|
|
14
|
+
_redact,
|
|
15
|
+
safe_mimo_config,
|
|
16
|
+
)
|
|
17
|
+
from mimo_qc_contract import ARTIFACT_NAME, DEFAULT_STAGE, MAX_FRAMES
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _semantic_evidence(evidence: Mapping[str, Any], *, stage: str) -> dict[str, Any]:
|
|
21
|
+
"""Keep real artifact values visible to MiMo without sending irrelevant paths.
|
|
22
|
+
|
|
23
|
+
``collect_evidence`` already bounds every artifact independently. The additional
|
|
24
|
+
summarization here therefore needs enough depth to retain the narration/ASR/TTS
|
|
25
|
+
scalars nested inside those summaries. A shallow second pass used to replace the
|
|
26
|
+
values with fingerprints, which made live QC invent missing-script and failed-TTS
|
|
27
|
+
findings from evidence it could no longer read.
|
|
28
|
+
"""
|
|
29
|
+
semantic = dict(evidence)
|
|
30
|
+
semantic.pop("work_dir", None)
|
|
31
|
+
semantic.pop("fingerprint", None)
|
|
32
|
+
semantic["evidence_roles"] = {
|
|
33
|
+
"narration.json": (
|
|
34
|
+
"Planned recap voiceover on the OUTPUT timeline; judge its factual and temporal "
|
|
35
|
+
"fit rather than expecting verbatim source dialogue."
|
|
36
|
+
),
|
|
37
|
+
"tts_meta.json": (
|
|
38
|
+
"Synthesis and placement metadata for narration.json; an empty failures list means "
|
|
39
|
+
"all requested narration segments synthesized successfully."
|
|
40
|
+
),
|
|
41
|
+
"source_asr": (
|
|
42
|
+
"Transcript of the SOURCE media audio, not a transcript of generated TTS. "
|
|
43
|
+
"Wording differences from narration are expected; use this evidence for factual "
|
|
44
|
+
"support and original-dialogue timing, not verbatim equality."
|
|
45
|
+
),
|
|
46
|
+
"generated_subtitles": (
|
|
47
|
+
"Recap subtitles derived from generated narration/TTS during assembly. They are not "
|
|
48
|
+
"source ASR and must never be used as independent factual support. Visible text that "
|
|
49
|
+
"matches one of these cues is the intended generated caption, including when it sits "
|
|
50
|
+
"on the black source-subtitle mask band."
|
|
51
|
+
),
|
|
52
|
+
"visual_metadata": (
|
|
53
|
+
"Diagnostic storyboard/frame metadata. labels_burned refers only to diagnostic "
|
|
54
|
+
"timestamp labels and does not mean labels were burned into the final video."
|
|
55
|
+
),
|
|
56
|
+
"final_output": (
|
|
57
|
+
"Only present for post_render and limited to candidates that actually exist."
|
|
58
|
+
),
|
|
59
|
+
"post_render_frame_limits": (
|
|
60
|
+
"Sampled final-output frames are silent still images: they contain no audible audio. "
|
|
61
|
+
"Do not infer which audio track is playing from visible source captions. Narration/TTS "
|
|
62
|
+
"timings are segment-level, not word-level, so an internal phrase cannot be assigned to "
|
|
63
|
+
"an exact sampled-frame timestamp."
|
|
64
|
+
),
|
|
65
|
+
"narration_and_subtitle_gaps": (
|
|
66
|
+
"Generated narration/subtitle gaps may be deliberate original-audio blocks. In those "
|
|
67
|
+
"gaps the source audio and its existing captions can intentionally return; do not call "
|
|
68
|
+
"the absence of generated subtitles a defect without evidence of accidental silence."
|
|
69
|
+
),
|
|
70
|
+
"actual_audio_timing": (
|
|
71
|
+
"narration.json start/end values are authoring slots, not proof that speech fills the "
|
|
72
|
+
"whole slot. For post-render timing, assembly_manifest audio_segments "
|
|
73
|
+
"actual_place_start/actual_place_end are authoritative. A sampled frame after "
|
|
74
|
+
"actual_place_end is in an original-audio gap."
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
if stage != "post_render":
|
|
78
|
+
semantic.pop("final_output", None)
|
|
79
|
+
# These files can be leftovers from an earlier assembly when narration is being revised.
|
|
80
|
+
# Pre-assemble QC must evaluate current narration/TTS, not stale rendered captions.
|
|
81
|
+
semantic.pop("generated_subtitles", None)
|
|
82
|
+
semantic["evidence_roles"].pop("generated_subtitles", None)
|
|
83
|
+
semantic["evidence_roles"].pop("post_render_frame_limits", None)
|
|
84
|
+
semantic["evidence_roles"].pop("actual_audio_timing", None)
|
|
85
|
+
else:
|
|
86
|
+
final_output = semantic.get("final_output")
|
|
87
|
+
candidates = (
|
|
88
|
+
final_output.get("candidates", [])
|
|
89
|
+
if isinstance(final_output, Mapping)
|
|
90
|
+
else []
|
|
91
|
+
)
|
|
92
|
+
semantic["final_output"] = {
|
|
93
|
+
"candidates": [
|
|
94
|
+
dict(candidate)
|
|
95
|
+
for candidate in candidates
|
|
96
|
+
if isinstance(candidate, Mapping) and candidate.get("exists") is True
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
# Artifact summaries are already bounded at collection time; summarizing them again
|
|
100
|
+
# would wrap their ``items`` arrays in another summary layer and obscure the values.
|
|
101
|
+
return _redact(semantic)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def build_payload(
|
|
105
|
+
evidence: Mapping[str, Any],
|
|
106
|
+
*,
|
|
107
|
+
stage: str = DEFAULT_STAGE,
|
|
108
|
+
config: Mapping[str, Any] | None = None,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
"""Build the report-safe semantic payload (never contains image base64)."""
|
|
111
|
+
cfg = safe_mimo_config(config)
|
|
112
|
+
payload = {
|
|
113
|
+
"stage": stage,
|
|
114
|
+
"artifact": ARTIFACT_NAME,
|
|
115
|
+
"model": cfg["model"],
|
|
116
|
+
"config": cfg,
|
|
117
|
+
"instructions": (
|
|
118
|
+
"你是 video-recap 的建议性质量审阅器。结合解说、剪辑计划、字幕/ASR、TTS、"
|
|
119
|
+
"组装元数据和抽样画面,指出语义或审美问题。只返回主观观察,不做自动修复,"
|
|
120
|
+
"只根据可见的实际字段判断,不得从文件字节数、指纹、截断或省略标记推断内容缺失;"
|
|
121
|
+
"source_asr 是源素材原声证据,不是生成后 TTS 的逐字转录;generated_subtitles 是本轮生成字幕,"
|
|
122
|
+
"二者角色不可混淆,解说改写与源台词不一致本身不是问题;"
|
|
123
|
+
"空 failures 表示没有失败,storyboard 的 labels_burned 仅表示诊断图上的时间标签,"
|
|
124
|
+
"不代表标签烧进最终视频;pre_assemble 阶段尚无 final_output 属于正常状态;"
|
|
125
|
+
"成片抽样画面是带时间戳的稀疏点样本,只能判断该具体时刻,不得用单帧否定整段内其他时刻的画面。"
|
|
126
|
+
"这些抽样帧是无声静帧,不能从画面中的源字幕推断当时实际播放哪条音轨;"
|
|
127
|
+
"源字幕遮罩策略为 off 时,保留的源字幕无需与解说逐字一致;解说/TTS 只有段级时间,"
|
|
128
|
+
"没有词级对齐,不得把句中某个短语强行对应到抽样帧的精确秒数。"
|
|
129
|
+
"生成解说和字幕之间的空档可能是刻意保留的原声块,此时原声和源字幕回归是正常设计,"
|
|
130
|
+
"没有意外静音证据时不得把生成字幕空档当成缺失。只输出可采取行动的疑似问题,"
|
|
131
|
+
"不要把正常、一致、相符或通过项作为 observation。不得提出阻断决定。"
|
|
132
|
+
"narration.json 的 start/end 是创作槽位,不表示旁白铺满整段;成片阶段必须以 "
|
|
133
|
+
"assembly_manifest.audio_segments 的 actual_place_start/actual_place_end 判断实际旁白。"
|
|
134
|
+
"抽样点超过 actual_place_end 时属于原声块;一个字幕时间段含多个分句时,静帧匹配其中任一分句都不算错位。"
|
|
135
|
+
"只有同一编号静帧中同时清晰可读两层不同字幕,才能报告字幕重叠;不同静帧分别出现解说字幕和源字幕不算重叠。"
|
|
136
|
+
"遮罩 opacity=1.0 表示源字幕像素已被不透明覆盖,不得臆测遮罩下仍有可见文字。"
|
|
137
|
+
"黑色遮罩带上与 generated_subtitles cue 一致的白字是本轮预期生成字幕,不是残留源字幕;"
|
|
138
|
+
"必须先逐字对照 generated_subtitles,再判断是否另有第二层源字幕。"
|
|
139
|
+
"每张抽样图都由同编号 BEGIN/END 文本包围,必须按编号和时间戳独立判断,"
|
|
140
|
+
"不得把一张图里的文字或人物归到另一张图。"
|
|
141
|
+
'返回 JSON:{"observations":[{"code":...,'
|
|
142
|
+
'"message":...,"category":"semantic|aesthetic",'
|
|
143
|
+
'"confidence":"low|medium|high","sample_policy":'
|
|
144
|
+
'"semantic|aesthetic|sampled","evidence":{...}}]}。最多 12 条。'
|
|
145
|
+
),
|
|
146
|
+
"evidence": _semantic_evidence(evidence, stage=stage),
|
|
147
|
+
"evidence_fingerprint": evidence.get("fingerprint")
|
|
148
|
+
or _fingerprint_value(_cache_evidence(evidence)),
|
|
149
|
+
}
|
|
150
|
+
payload["payload_fingerprint"] = _fingerprint_value(payload)
|
|
151
|
+
return _redact(payload)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _request_payload(
|
|
155
|
+
payload: Mapping[str, Any], frame_samples: Sequence[Mapping[str, Any]]
|
|
156
|
+
) -> dict[str, Any]:
|
|
157
|
+
request_evidence = {
|
|
158
|
+
"stage": payload["stage"],
|
|
159
|
+
"instructions": payload["instructions"],
|
|
160
|
+
"evidence": payload["evidence"],
|
|
161
|
+
}
|
|
162
|
+
content: list[dict[str, Any]] = [
|
|
163
|
+
{
|
|
164
|
+
"type": "text",
|
|
165
|
+
"text": json.dumps(
|
|
166
|
+
request_evidence, ensure_ascii=False, separators=(",", ":")
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
for index, sample in enumerate(list(frame_samples)[:MAX_FRAMES], start=1):
|
|
171
|
+
data_url = sample.get("data_url") if isinstance(sample, Mapping) else None
|
|
172
|
+
if isinstance(data_url, str) and data_url.startswith("data:image/jpeg;base64,"):
|
|
173
|
+
timestamp = sample.get("timestamp")
|
|
174
|
+
try:
|
|
175
|
+
timestamp_label = f"{float(timestamp):.3f}s"
|
|
176
|
+
except (TypeError, ValueError):
|
|
177
|
+
timestamp_label = "unknown"
|
|
178
|
+
content.append(
|
|
179
|
+
{
|
|
180
|
+
"type": "text",
|
|
181
|
+
"text": (
|
|
182
|
+
f"BEGIN QC_FRAME_{index}: final-output timestamp {timestamp_label}. "
|
|
183
|
+
f"Judge only this image and do not transfer its content to another frame."
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
content.append({"type": "image_url", "image_url": {"url": data_url}})
|
|
188
|
+
content.append(
|
|
189
|
+
{
|
|
190
|
+
"type": "text",
|
|
191
|
+
"text": f"END QC_FRAME_{index}: timestamp {timestamp_label}.",
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
return {
|
|
195
|
+
"model": payload["model"],
|
|
196
|
+
"messages": [{"role": "user", "content": content}],
|
|
197
|
+
"max_completion_tokens": 1600,
|
|
198
|
+
"thinking": {"type": "disabled"},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _strip_json_fence(text: str) -> str:
|
|
203
|
+
value = text.strip()
|
|
204
|
+
if value.startswith("```") and value.endswith("```"):
|
|
205
|
+
lines = value.splitlines()
|
|
206
|
+
if len(lines) >= 3:
|
|
207
|
+
value = "\n".join(lines[1:-1]).strip()
|
|
208
|
+
return value
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _validated_live_output(response: Any) -> Any:
|
|
212
|
+
try:
|
|
213
|
+
content = response["choices"][0]["message"]["content"]
|
|
214
|
+
except (KeyError, IndexError, TypeError):
|
|
215
|
+
raise ValueError("malformed_response") from None
|
|
216
|
+
if isinstance(content, str):
|
|
217
|
+
try:
|
|
218
|
+
content = json.loads(_strip_json_fence(content))
|
|
219
|
+
except (TypeError, ValueError):
|
|
220
|
+
raise ValueError("malformed_json_content") from None
|
|
221
|
+
if isinstance(content, list):
|
|
222
|
+
return content
|
|
223
|
+
if isinstance(content, Mapping) and isinstance(content.get("observations"), list):
|
|
224
|
+
return content
|
|
225
|
+
if isinstance(content, Mapping) and isinstance(content.get("findings"), list):
|
|
226
|
+
return content
|
|
227
|
+
raise ValueError("malformed_observations")
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Sample frames and build aggregate MiMo QC reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
import subprocess
|
|
18
|
+
|
|
19
|
+
import tempfile
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
24
|
+
|
|
25
|
+
import qc_contract
|
|
26
|
+
|
|
27
|
+
from mimo_qc_evidence import (
|
|
28
|
+
_cache_evidence,
|
|
29
|
+
_effective_config,
|
|
30
|
+
_existing_final_output,
|
|
31
|
+
_fingerprint_value,
|
|
32
|
+
_redact,
|
|
33
|
+
collect_evidence,
|
|
34
|
+
safe_mimo_config,
|
|
35
|
+
)
|
|
36
|
+
from mimo_qc_observations import normalize_observations
|
|
37
|
+
from mimo_qc_payload import _request_payload, _validated_live_output, build_payload
|
|
38
|
+
from mimo_qc_client import mimo_qc_api_call
|
|
39
|
+
from mimo_qc_contract import (
|
|
40
|
+
ARTIFACT_NAME,
|
|
41
|
+
DEFAULT_STAGE,
|
|
42
|
+
FRAME_SAMPLER_VERSION,
|
|
43
|
+
MAX_FRAME_DIMENSION,
|
|
44
|
+
MAX_FRAMES,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
JudgeCallable = Callable[
|
|
48
|
+
[Mapping[str, Any]], Mapping[str, Any] | Sequence[Mapping[str, Any]]
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
FrameSampler = Callable[..., Sequence[Mapping[str, Any]]]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _probe_duration(path: Path) -> float:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[
|
|
57
|
+
"ffprobe",
|
|
58
|
+
"-v",
|
|
59
|
+
"error",
|
|
60
|
+
"-show_entries",
|
|
61
|
+
"format=duration",
|
|
62
|
+
"-of",
|
|
63
|
+
"json",
|
|
64
|
+
str(path),
|
|
65
|
+
],
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=20,
|
|
69
|
+
check=False,
|
|
70
|
+
)
|
|
71
|
+
if result.returncode != 0:
|
|
72
|
+
return 0.0
|
|
73
|
+
try:
|
|
74
|
+
return max(0.0, float(json.loads(result.stdout)["format"]["duration"]))
|
|
75
|
+
except (KeyError, TypeError, ValueError, json.JSONDecodeError):
|
|
76
|
+
return 0.0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def sample_video_frames(
|
|
80
|
+
video_path: str | Path,
|
|
81
|
+
*,
|
|
82
|
+
max_frames: int = MAX_FRAMES,
|
|
83
|
+
max_dimension: int = MAX_FRAME_DIMENSION,
|
|
84
|
+
) -> list[dict[str, Any]]:
|
|
85
|
+
"""Extract at most six bounded JPEGs; returned base64 is request-only."""
|
|
86
|
+
path = Path(video_path)
|
|
87
|
+
if not path.is_file() or max_frames <= 0:
|
|
88
|
+
return []
|
|
89
|
+
try:
|
|
90
|
+
duration = _probe_duration(path)
|
|
91
|
+
except (OSError, subprocess.SubprocessError):
|
|
92
|
+
return []
|
|
93
|
+
if duration <= 0:
|
|
94
|
+
return []
|
|
95
|
+
count = min(int(max_frames), max(1, int(math.ceil(duration / 15.0))))
|
|
96
|
+
samples = []
|
|
97
|
+
with tempfile.TemporaryDirectory(prefix="video-recap-mimo-qc-") as tmp:
|
|
98
|
+
for index in range(count):
|
|
99
|
+
timestamp = duration * (index + 0.5) / count
|
|
100
|
+
destination = Path(tmp) / f"frame-{index:02d}.jpg"
|
|
101
|
+
try:
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
[
|
|
104
|
+
"ffmpeg",
|
|
105
|
+
"-hide_banner",
|
|
106
|
+
"-loglevel",
|
|
107
|
+
"error",
|
|
108
|
+
"-y",
|
|
109
|
+
"-ss",
|
|
110
|
+
f"{timestamp:.3f}",
|
|
111
|
+
"-i",
|
|
112
|
+
str(path),
|
|
113
|
+
"-frames:v",
|
|
114
|
+
"1",
|
|
115
|
+
"-vf",
|
|
116
|
+
(
|
|
117
|
+
f"scale={int(max_dimension)}:{int(max_dimension)}:"
|
|
118
|
+
"force_original_aspect_ratio=decrease"
|
|
119
|
+
),
|
|
120
|
+
"-q:v",
|
|
121
|
+
"4",
|
|
122
|
+
str(destination),
|
|
123
|
+
],
|
|
124
|
+
capture_output=True,
|
|
125
|
+
timeout=30,
|
|
126
|
+
check=False,
|
|
127
|
+
)
|
|
128
|
+
except (OSError, subprocess.SubprocessError):
|
|
129
|
+
continue
|
|
130
|
+
if result.returncode != 0 or not destination.is_file():
|
|
131
|
+
continue
|
|
132
|
+
raw = destination.read_bytes()
|
|
133
|
+
samples.append(
|
|
134
|
+
{
|
|
135
|
+
"data_url": "data:image/jpeg;base64,"
|
|
136
|
+
+ base64.b64encode(raw).decode("ascii"),
|
|
137
|
+
"sha256": hashlib.sha256(raw).hexdigest(),
|
|
138
|
+
"timestamp": round(timestamp, 3),
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
return samples[:MAX_FRAMES]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _frame_metadata(samples: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
|
|
145
|
+
return {
|
|
146
|
+
"count": min(len(samples), MAX_FRAMES),
|
|
147
|
+
"max_frames": MAX_FRAMES,
|
|
148
|
+
"max_dimension": MAX_FRAME_DIMENSION,
|
|
149
|
+
"sampler_version": FRAME_SAMPLER_VERSION,
|
|
150
|
+
"samples": [
|
|
151
|
+
{
|
|
152
|
+
"sha256": sample.get("sha256"),
|
|
153
|
+
"timestamp": sample.get("timestamp"),
|
|
154
|
+
}
|
|
155
|
+
for sample in list(samples)[:MAX_FRAMES]
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _cache_input(
|
|
161
|
+
stage: str,
|
|
162
|
+
payload: Mapping[str, Any],
|
|
163
|
+
evidence: Mapping[str, Any],
|
|
164
|
+
frames: Mapping[str, Any],
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
return {
|
|
167
|
+
"stage": stage,
|
|
168
|
+
"model": payload.get("model"),
|
|
169
|
+
"payload_fingerprint": payload.get("payload_fingerprint"),
|
|
170
|
+
"evidence": _cache_evidence(evidence),
|
|
171
|
+
"frames": frames,
|
|
172
|
+
"contract": qc_contract.SCHEMA_VERSION,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _stage_reports(path: Path) -> dict[str, dict[str, Any]]:
|
|
177
|
+
if not path.is_file():
|
|
178
|
+
return {}
|
|
179
|
+
try:
|
|
180
|
+
report = json.loads(path.read_text(encoding="utf-8"))
|
|
181
|
+
qc_contract.validate_report(report)
|
|
182
|
+
except (OSError, ValueError, TypeError, qc_contract.QCContractError):
|
|
183
|
+
return {}
|
|
184
|
+
stages = report.get("metadata", {}).get("stages")
|
|
185
|
+
if isinstance(stages, Mapping):
|
|
186
|
+
valid = {}
|
|
187
|
+
for stage, stage_report in stages.items():
|
|
188
|
+
try:
|
|
189
|
+
qc_contract.validate_report(stage_report)
|
|
190
|
+
except (TypeError, qc_contract.QCContractError):
|
|
191
|
+
continue
|
|
192
|
+
valid[str(stage)] = dict(stage_report)
|
|
193
|
+
return valid
|
|
194
|
+
return {str(report["stage"]): report}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _error_name(exc: Exception) -> str:
|
|
198
|
+
text = str(_redact(str(exc))).strip()
|
|
199
|
+
return (text or type(exc).__name__)[:120]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def build_report(
|
|
203
|
+
work_dir: str | Path,
|
|
204
|
+
*,
|
|
205
|
+
stage: str = DEFAULT_STAGE,
|
|
206
|
+
fixture: Any | None = None,
|
|
207
|
+
dry_run: bool = False,
|
|
208
|
+
judge: JudgeCallable | None = None,
|
|
209
|
+
config: Mapping[str, Any] | None = None,
|
|
210
|
+
final_output: str | Path | None = None,
|
|
211
|
+
live: bool = False,
|
|
212
|
+
refresh: bool = False,
|
|
213
|
+
frame_sampler: FrameSampler | None = None,
|
|
214
|
+
existing: Mapping[str, Any] | None = None,
|
|
215
|
+
api_call: Callable[..., Any] | None = None,
|
|
216
|
+
) -> dict[str, Any]:
|
|
217
|
+
"""Build one validated stage report; all live failures remain successful QC."""
|
|
218
|
+
root = Path(work_dir)
|
|
219
|
+
evidence = collect_evidence(root, final_output=final_output)
|
|
220
|
+
payload = build_payload(evidence, stage=stage, config=config)
|
|
221
|
+
cfg = safe_mimo_config(config)
|
|
222
|
+
samples: Sequence[Mapping[str, Any]] = []
|
|
223
|
+
if stage == "post_render" and (fixture is not None or judge is not None or live):
|
|
224
|
+
output = _existing_final_output(root, final_output)
|
|
225
|
+
if output is not None:
|
|
226
|
+
sampler = frame_sampler or sample_video_frames
|
|
227
|
+
try:
|
|
228
|
+
samples = list(
|
|
229
|
+
sampler(
|
|
230
|
+
output, max_frames=MAX_FRAMES, max_dimension=MAX_FRAME_DIMENSION
|
|
231
|
+
)
|
|
232
|
+
)[:MAX_FRAMES]
|
|
233
|
+
except Exception:
|
|
234
|
+
samples = []
|
|
235
|
+
frame_meta = _frame_metadata(samples)
|
|
236
|
+
cache_input = _cache_input(stage, payload, evidence, frame_meta)
|
|
237
|
+
cache_key = _fingerprint_value(cache_input)
|
|
238
|
+
|
|
239
|
+
if (
|
|
240
|
+
not refresh
|
|
241
|
+
and existing
|
|
242
|
+
and existing.get("metadata", {}).get("cache_key") == cache_key
|
|
243
|
+
):
|
|
244
|
+
cached = json.loads(json.dumps(existing))
|
|
245
|
+
cached["metadata"]["status"] = "cached"
|
|
246
|
+
cached["metadata"]["mode"] = "live_cache"
|
|
247
|
+
cached["metadata"]["request_count"] = 0
|
|
248
|
+
qc_contract.validate_report(cached)
|
|
249
|
+
return cached
|
|
250
|
+
|
|
251
|
+
status = "completed"
|
|
252
|
+
error = None
|
|
253
|
+
if fixture is not None:
|
|
254
|
+
model_output = fixture
|
|
255
|
+
mode = "fixture"
|
|
256
|
+
elif judge is not None and not dry_run:
|
|
257
|
+
mode = "injected_judge"
|
|
258
|
+
try:
|
|
259
|
+
model_output = judge(payload)
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
model_output = {"observations": []}
|
|
262
|
+
status, error = "failed", _error_name(exc)
|
|
263
|
+
elif live and not dry_run:
|
|
264
|
+
mode = "live"
|
|
265
|
+
if not cfg["key_present"]:
|
|
266
|
+
model_output = {"observations": []}
|
|
267
|
+
status, error = "unavailable", "missing_key"
|
|
268
|
+
else:
|
|
269
|
+
try:
|
|
270
|
+
response = (api_call or mimo_qc_api_call)(
|
|
271
|
+
_request_payload(payload, samples),
|
|
272
|
+
config=_effective_config(config),
|
|
273
|
+
timeout=60,
|
|
274
|
+
)
|
|
275
|
+
model_output = _validated_live_output(response)
|
|
276
|
+
except Exception as exc:
|
|
277
|
+
model_output = {"observations": []}
|
|
278
|
+
status, error = "failed", _error_name(exc)
|
|
279
|
+
else:
|
|
280
|
+
model_output = {"observations": []}
|
|
281
|
+
mode = "dry_run"
|
|
282
|
+
status = "dry_run"
|
|
283
|
+
|
|
284
|
+
findings = normalize_observations(
|
|
285
|
+
model_output, stage=stage, payload=payload, model_config=config
|
|
286
|
+
)
|
|
287
|
+
metadata = {
|
|
288
|
+
"mode": mode,
|
|
289
|
+
"status": status,
|
|
290
|
+
"error": error,
|
|
291
|
+
"report_only": True,
|
|
292
|
+
"auto_repair": False,
|
|
293
|
+
"pipeline_blocking": False,
|
|
294
|
+
"deterministic": False,
|
|
295
|
+
"request_count": 1
|
|
296
|
+
if mode == "live" and status in {"completed", "failed"}
|
|
297
|
+
else 0,
|
|
298
|
+
"cache_key": cache_key,
|
|
299
|
+
"cache_input": cache_input,
|
|
300
|
+
"frame_samples": frame_meta,
|
|
301
|
+
"evidence": evidence,
|
|
302
|
+
"payload": payload,
|
|
303
|
+
"config": cfg,
|
|
304
|
+
}
|
|
305
|
+
report = qc_contract.build_report(
|
|
306
|
+
artifact=ARTIFACT_NAME,
|
|
307
|
+
stage=stage,
|
|
308
|
+
findings=findings,
|
|
309
|
+
metadata=metadata,
|
|
310
|
+
)
|
|
311
|
+
qc_contract.validate_report(report)
|
|
312
|
+
return _redact(report)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _aggregate_reports(
|
|
316
|
+
stage_reports: Mapping[str, Mapping[str, Any]], current_stage: str
|
|
317
|
+
) -> dict[str, Any]:
|
|
318
|
+
current = stage_reports[current_stage]
|
|
319
|
+
findings = [
|
|
320
|
+
finding
|
|
321
|
+
for stage_report in stage_reports.values()
|
|
322
|
+
for finding in stage_report.get("findings", [])
|
|
323
|
+
]
|
|
324
|
+
metadata = dict(current.get("metadata", {}))
|
|
325
|
+
metadata["stages"] = {
|
|
326
|
+
stage: dict(report) for stage, report in stage_reports.items()
|
|
327
|
+
}
|
|
328
|
+
return qc_contract.build_report(
|
|
329
|
+
artifact=ARTIFACT_NAME,
|
|
330
|
+
stage=current_stage,
|
|
331
|
+
findings=findings,
|
|
332
|
+
metadata=metadata,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def write_report(
|
|
337
|
+
work_dir: str | Path, report: Mapping[str, Any], *, output: str | Path | None = None
|
|
338
|
+
) -> tuple[Path, dict[str, Any]]:
|
|
339
|
+
"""Atomically merge one stage into mimo_qc.json and return the aggregate."""
|
|
340
|
+
path = Path(output) if output else Path(work_dir) / ARTIFACT_NAME
|
|
341
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
342
|
+
safe_stage = _redact(dict(report))
|
|
343
|
+
qc_contract.validate_report(safe_stage)
|
|
344
|
+
stages = _stage_reports(path)
|
|
345
|
+
stages[safe_stage["stage"]] = safe_stage
|
|
346
|
+
aggregate = _aggregate_reports(stages, safe_stage["stage"])
|
|
347
|
+
qc_contract.validate_report(aggregate)
|
|
348
|
+
serialized = json.dumps(_redact(aggregate), ensure_ascii=False, indent=2) + "\n"
|
|
349
|
+
descriptor, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
|
|
350
|
+
try:
|
|
351
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
|
352
|
+
handle.write(serialized)
|
|
353
|
+
handle.flush()
|
|
354
|
+
os.fsync(handle.fileno())
|
|
355
|
+
os.replace(temp_name, path)
|
|
356
|
+
except Exception:
|
|
357
|
+
try:
|
|
358
|
+
os.unlink(temp_name)
|
|
359
|
+
except OSError:
|
|
360
|
+
pass
|
|
361
|
+
raise
|
|
362
|
+
return path, aggregate
|