@oh-story/dsh 0.1.10 → 0.1.12
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 +11 -11
- package/lib/client.js +7 -3
- package/lib/client.js.map +2 -2
- package/lib/drama/manifest.json +212 -142
- package/lib/drama/skills/short-drama/SKILL.md +14 -2
- package/lib/drama/skills/short-drama/assets/project-template/short-drama.json +3 -0
- package/lib/drama/skills/short-drama/references/creator-documents.md +3 -0
- package/lib/drama/skills/short-drama/references/knowhow-index.md +15 -0
- package/lib/drama/skills/short-drama/scripts/project_tool.py +131 -0
- package/lib/drama/skills/short-drama-assets/SKILL.md +2 -1
- package/lib/drama/skills/short-drama-assets/references/asset-review-checklist.md +2 -0
- package/lib/drama/skills/short-drama-assets/references/era-anchors.md +37 -0
- package/lib/drama/skills/short-drama-assets/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-develop/SKILL.md +10 -4
- package/lib/drama/skills/short-drama-develop/assets/adaptation-map.example.jsonl +1 -1
- package/lib/drama/skills/short-drama-develop/assets/creative-brief.md +8 -0
- package/lib/drama/skills/short-drama-develop/references/adaptation-craft.md +80 -13
- package/lib/drama/skills/short-drama-develop/references/episode-design.md +95 -12
- package/lib/drama/skills/short-drama-develop/references/genre-and-hook-playbook.md +9 -5
- package/lib/drama/skills/short-drama-develop/references/genre-cards//345/244/215/344/273/207/346/211/223/350/204/270.md +1 -1
- package/lib/drama/skills/short-drama-develop/references/genre-cards//347/263/273/347/273/237/346/265/201.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/200/206/350/242/255.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards//351/207/215/347/224/237/347/251/277/350/266/212.md +51 -0
- package/lib/drama/skills/short-drama-develop/references/genre-cards.md +10 -7
- package/lib/drama/skills/short-drama-develop/references/premise-devices.md +15 -9
- package/lib/drama/skills/short-drama-develop/references/serial-character-and-memory.md +3 -2
- package/lib/drama/skills/short-drama-develop/references/stage-contract.md +14 -7
- package/lib/drama/skills/short-drama-develop/references/story-craft.md +7 -3
- package/lib/drama/skills/short-drama-edit/SKILL.md +43 -20
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +49 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +4 -2
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Overlay.tsx +31 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +5 -5
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +68 -69
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +39 -29
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/rules.mjs +188 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +26 -13
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/Card.tsx +72 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/CornerChip.tsx +39 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/ScreenText.tsx +36 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/SystemPanel.tsx +44 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/TaskPanel.tsx +100 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/chrome.tsx +197 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/screen/tokens.ts +62 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +1 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +4 -2
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +3 -1
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +6 -2
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +12 -6
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +13 -8
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1153 -121
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +49 -9
- package/lib/drama/skills/short-drama-novel-analyze/SKILL.md +3 -3
- package/lib/drama/skills/short-drama-novel-analyze/assets/episode-candidate.example.jsonl +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-triage.md +8 -4
- package/lib/drama/skills/short-drama-novel-analyze/references/adaptation-value.md +10 -3
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +25 -1
- package/lib/drama/skills/short-drama-novel-analyze/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -2
- package/lib/drama/skills/short-drama-review/references/review-method.md +6 -1
- package/lib/drama/skills/short-drama-review/references/rubric-assets-prompts.md +11 -0
- package/lib/drama/skills/short-drama-review/references/rubric-story-script.md +77 -3
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +15 -0
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +13 -5
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +2 -2
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +82 -14
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +5 -3
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +12 -0
- package/lib/drama/skills/short-drama-video-prompts/references/delivery-profile.md +4 -5
- package/lib/drama/skills/short-drama-video-prompts/references/generability.md +4 -2
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +40 -1
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +20 -14
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +8 -7
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +8 -1
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +103 -0
- package/lib/drama/skills/short-drama-video-prompts/scripts/motion_timing_check.py +8 -7
- package/lib/drama/skills/short-drama-write/SKILL.md +8 -3
- package/lib/drama/skills/short-drama-write/references/dialogue-craft.md +1 -1
- package/lib/drama/skills/short-drama-write/references/script-craft.md +56 -3
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +4 -0
- package/lib/drama/skills/short-drama-write/scripts/duration_estimate.py +48 -2
- package/lib/index.js +71 -21
- package/lib/index.js.map +3 -3
- package/lib/oh-story/manifest.json +545 -310
- package/lib/oh-story/roles/chapter-extractor.md +11 -11
- package/lib/oh-story/roles/character-designer.md +1 -2
- package/lib/oh-story/roles/consistency-checker.md +11 -9
- package/lib/oh-story/roles/narrative-writer.md +24 -25
- package/lib/oh-story/roles/story-architect.md +9 -61
- package/lib/oh-story/roles/story-explorer.md +20 -125
- package/lib/oh-story/roles/story-researcher.md +0 -1
- package/lib/oh-story/skills/story/SKILL.md +6 -6
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/SKILL.md +24 -25
- package/lib/oh-story/skills/story-deslop/references/agent-calls.md +5 -0
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-deslop/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-deslop/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-import/SKILL.md +63 -579
- package/lib/oh-story/skills/story-import/references/deep-analysis.md +47 -0
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +1 -1
- package/lib/oh-story/skills/story-import/references/import-report.md +64 -0
- package/lib/oh-story/skills/story-import/references/import-special-cases.md +34 -0
- package/lib/oh-story/skills/story-import/references/import-tracking.md +95 -0
- package/lib/oh-story/skills/story-import/references/length-routing.md +1 -1
- package/lib/oh-story/skills/story-import/references/outline-reverse-rules.md +79 -0
- package/lib/oh-story/skills/story-import/references/outline-reverse.md +24 -0
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +95 -91
- package/lib/oh-story/skills/story-import/references/structure-mapping-short.md +24 -12
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-import/scripts/build_outline_brief.py +332 -0
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +28 -50
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +12 -5
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/final-checks.md +19 -0
- package/lib/oh-story/skills/story-long-analyze/references/index-rebuild.md +11 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +21 -55
- package/lib/oh-story/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage2-extraction.md +229 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
- package/lib/oh-story/skills/story-long-analyze/references/stage5-report.md +33 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-generator.md +17 -0
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +1 -1
- package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +47 -8
- package/lib/oh-story/skills/story-long-scan/SKILL.md +41 -193
- package/lib/oh-story/skills/story-long-scan/references/platform-ciweimao.md +19 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-fanqie.md +45 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-jjwxc.md +28 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qidian.md +35 -0
- package/lib/oh-story/skills/story-long-scan/references/platform-qimao.md +27 -0
- package/lib/oh-story/skills/story-long-scan/references/topic-decision.md +6 -2
- package/lib/oh-story/skills/story-long-write/SKILL.md +32 -73
- package/lib/oh-story/skills/story-long-write/references/agent-calls.md +47 -0
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +18 -201
- package/lib/oh-story/skills/story-long-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +6 -10
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-long-write/references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-long-write/references/genre-prose-cards.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +3 -2
- package/lib/oh-story/skills/story-long-write/references/long-format.md +0 -8
- package/lib/oh-story/skills/story-long-write/references/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-long-write/references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +14 -69
- package/lib/oh-story/skills/story-long-write/references/plot-frameworks.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/plot-special-topics.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/project-files.md +10 -6
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/stop-and-fix.md +9 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -58
- package/lib/oh-story/skills/story-long-write/references/style-genre-modules.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +33 -44
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +18 -51
- package/lib/oh-story/skills/story-long-write/references/workflow-outline.md +157 -0
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +53 -67
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -251
- package/lib/oh-story/skills/story-long-write/references/workflow-volume.md +142 -0
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +5 -3
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-long-write/scripts/build_architect_brief.py +260 -0
- package/lib/oh-story/skills/story-long-write/scripts/build_writer_prompt.py +96 -16
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +101 -14
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +7 -2
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +2 -1
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +250 -50
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-review/SKILL.md +19 -273
- package/lib/oh-story/skills/story-review/references/agent-prompts.md +184 -0
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-review/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-review/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-review/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-review/references/batch-review.md +19 -0
- package/lib/oh-story/skills/story-review/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-review/references/review-quality.md +2 -1
- package/lib/oh-story/skills/story-review/references/review-tracking.md +10 -0
- package/lib/oh-story/skills/story-review/references/solo.md +52 -0
- package/lib/oh-story/skills/story-review/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +1 -1
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +11 -9
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-review/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +278 -23
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +179 -23
- package/lib/oh-story/skills/story-setup/SKILL.md +69 -458
- package/lib/oh-story/skills/story-setup/references/agent-references/agent-quality.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +21 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/emotional-arc-design.md +1 -114
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +1 -1
- 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/long-genre-catalog.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/long-genre-mechanics.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/long-quality.md +7 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/opening-design.md +2 -2
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-conflict.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-methods.md +7 -7
- package/lib/oh-story/skills/story-setup/references/agent-references/outline-rhythm.md +3 -3
- package/lib/oh-story/skills/story-setup/references/agent-references/short-emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +0 -15
- package/lib/oh-story/skills/story-setup/references/agent-references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -58
- 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/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +18 -56
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +3 -3
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +37 -110
- package/lib/oh-story/skills/story-short-analyze/references/analysis-character-relations.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/analysis-method.md +42 -0
- package/lib/oh-story/skills/story-short-analyze/references/optional-modules.md +97 -0
- package/lib/oh-story/skills/story-short-analyze/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-analyze/references/quality-checklist.md +80 -0
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
- package/lib/oh-story/skills/story-short-analyze/references/{material-decomposition.md → stage4-6-reversal-summary.md} +303 -374
- package/lib/oh-story/skills/story-short-scan/SKILL.md +30 -64
- package/lib/oh-story/skills/story-short-scan/references/manual-rank-input.md +36 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-dianzhong.md +24 -0
- package/lib/oh-story/skills/story-short-scan/references/platform-heiyan.md +21 -0
- package/lib/oh-story/skills/story-short-scan/references/topic-match.md +38 -0
- package/lib/oh-story/skills/story-short-write/SKILL.md +23 -34
- package/lib/oh-story/skills/story-short-write/references/agent-calls.md +13 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory-maintenance.md +75 -0
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +40 -118
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +1 -21
- package/lib/oh-story/skills/story-short-write/references/emotional-methods.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +0 -15
- package/lib/oh-story/skills/story-short-write/references/output-contract.md +9 -47
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +17 -49
- package/lib/oh-story/skills/story-short-write/references/scene-craft.md +40 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +7 -43
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -1
- package/lib/oh-story/skills/story-short-write/references/short-reversal.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/submission-craft.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/villain-and-reveal.md +5 -10
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +19 -12
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +22 -32
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +84 -17
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +7 -119
- package/lib/oh-story/skills/story-short-write/scripts/author_memory_commit.py +3 -3
- package/lib/oh-story/skills/story-short-write/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +7 -2
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +2 -2
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-tool.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/types/workspace-route.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +60 -1
- package/lib/video-recap/README.en.md +348 -137
- package/lib/video-recap/README.md +340 -136
- package/lib/video-recap/manifest.json +406 -251
- package/lib/video-recap/skills/video-assemble/SKILL.md +30 -15
- package/lib/video-recap/skills/video-assemble/references/audio-modes.md +63 -0
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +75 -0
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +118 -0
- package/lib/video-recap/skills/video-assemble/references/narration-adoption.md +103 -0
- package/lib/video-recap/skills/video-assemble/references/packaging.md +45 -0
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +74 -0
- package/lib/video-recap/skills/video-assemble/references/source-score.md +147 -0
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +202 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +10 -46
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +378 -119
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +29 -12
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +95 -44
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +75 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +11 -11
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +293 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +71 -78
- package/lib/video-recap/skills/video-assemble/scripts/jianying/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_builders.py → jianying/builders.py} +39 -67
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_model.py → jianying/model.py} +3 -11
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_optional.py → jianying/optional.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/jianying/schema.py +53 -0
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_templates.py → jianying/templates.py} +1 -1
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_timeline_contract.py → jianying/timeline_contract.py} +34 -13
- package/lib/video-recap/skills/video-assemble/scripts/{jianying_writer.py → jianying/writer.py} +7 -17
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +37 -4
- package/lib/video-recap/skills/video-assemble/scripts/media.py +12 -21
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +64 -34
- package/lib/video-recap/skills/video-assemble/scripts/packaging.py +122 -0
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +216 -0
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +1 -3
- package/lib/video-recap/skills/video-assemble/scripts/source_score.py +469 -0
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +25 -9
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py} +13 -6
- package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py} +5 -4
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +324 -0
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +30 -46
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +91 -7
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +22 -8
- package/lib/video-recap/skills/video-cut/SKILL.md +34 -16
- package/lib/video-recap/skills/video-cut/references/shot-review.md +50 -0
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -28
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +58 -62
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +21 -106
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +14 -9
- package/lib/video-recap/skills/video-cut/scripts/lib.py +39 -0
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +2 -155
- package/lib/video-recap/skills/video-cut/scripts/narrative_selection.py +244 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +357 -0
- package/lib/video-recap/skills/video-recap/SKILL.md +86 -42
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +126 -0
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +12 -8
- package/lib/video-recap/skills/video-recap/references/data-schema.md +44 -33
- package/lib/video-recap/skills/video-recap/references/resource-library.md +151 -0
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +1 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +123 -30
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +22 -75
- package/lib/video-recap/skills/video-recap/scripts/lib.py +3 -3
- package/lib/video-recap/skills/video-recap/scripts/library.py +509 -0
- package/lib/video-recap/skills/video-recap/scripts/materials.py +63 -91
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +8 -70
- package/lib/video-recap/skills/video-recap/scripts/project_binding.py +286 -0
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +1 -0
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_client.py → qc/mimo_client.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_evidence.py → qc/mimo_evidence.py} +33 -66
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_observations.py → qc/mimo_observations.py} +3 -11
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_payload.py → qc/mimo_payload.py} +4 -7
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_report.py → qc/mimo_report.py} +17 -28
- package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_runner.py → qc/mimo_runner.py} +2 -2
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -19
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +119 -50
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +32 -114
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +9 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +387 -334
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +23 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +278 -0
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +25 -12
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +59 -52
- package/lib/video-recap/skills/video-recap/scripts/resource_lock.py +162 -0
- package/lib/video-recap/skills/video-script/SKILL.md +16 -13
- package/lib/video-recap/skills/video-script/references/creative-editing-playbook.md +9 -5
- package/lib/video-recap/skills/video-script/references/promotional-copy.md +68 -0
- package/lib/video-recap/skills/video-script/references/research-guide.md +1 -1
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +24 -76
- package/lib/video-recap/skills/video-script/scripts/lib.py +10 -87
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-script/scripts/review.py +0 -2
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +35 -59
- package/lib/video-recap/skills/video-script/scripts/review_response.py +66 -128
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +12 -20
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-script/scripts/validate.py +53 -42
- package/lib/video-recap/skills/video-understanding/SKILL.md +6 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +50 -156
- package/lib/video-recap/skills/video-understanding/references/research-guide.md +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +85 -65
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +2 -5
- package/lib/video-recap/skills/video-understanding/scripts/briefing/__init__.py +1 -0
- package/lib/video-recap/skills/video-understanding/scripts/{agent_brief.py → briefing/builder.py} +38 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_context.py → video-understanding/scripts/briefing/context.py} +4 -35
- package/lib/video-recap/skills/{video-script/scripts/brief_inputs.py → video-understanding/scripts/briefing/inputs.py} +9 -31
- package/lib/video-recap/skills/video-understanding/scripts/{brief_timeline.py → briefing/timeline.py} +5 -6
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +65 -116
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +31 -40
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +30 -86
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +63 -39
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +4 -4
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +17 -79
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +14 -40
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +64 -46
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +31 -11
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +19 -14
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +80 -108
- package/lib/video-recap/skills/video-voiceover/SKILL.md +29 -19
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +31 -0
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +51 -59
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +8 -44
- package/lib/video-recap/skills/video-voiceover/scripts/providers/__init__.py +1 -0
- package/lib/video-recap/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +212 -134
- package/package.json +41 -41
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -538
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -603
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
- package/lib/oh-story/skills/story-short-analyze/references/output-templates.md +0 -561
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +0 -98
- package/lib/video-recap/skills/video-recap/references/creative-editing-playbook.md +0 -225
- package/lib/video-recap/skills/video-recap/references/research-guide.md +0 -74
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +0 -250
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +0 -399
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +0 -327
- package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +0 -348
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
- /package/lib/video-recap/skills/video-assemble/scripts/{jianying_tracks.py → jianying/tracks.py} +0 -0
- /package/lib/video-recap/skills/video-recap/scripts/{mimo_qc_contract.py → qc/mimo_contract.py} +0 -0
- /package/lib/video-recap/skills/video-voiceover/scripts/{fish_audio.py → providers/fish_audio.py} +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Validate explicitly required source spans in a normalized constant-speed cut plan.
|
|
2
|
+
|
|
3
|
+
This checks source-span selection and ordering only. It does not claim that the
|
|
4
|
+
rendered mix is audible or that the declared content is semantically correct.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
from typing import TypeGuard
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_EPSILON = 1e-9
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _invalid(message):
|
|
16
|
+
return {
|
|
17
|
+
"selection_status": "BLOCK",
|
|
18
|
+
"nodes": [],
|
|
19
|
+
"findings": [
|
|
20
|
+
{
|
|
21
|
+
"code": "REQUIRED_EVIDENCE_INVALID",
|
|
22
|
+
"message": message,
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"semantic_status": "NOT_CHECKED",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _realpath(value, *, require_absolute=True):
|
|
30
|
+
if not isinstance(value, str) or (require_absolute and not os.path.isabs(value)):
|
|
31
|
+
raise ValueError("required evidence source must be an absolute path")
|
|
32
|
+
return os.path.realpath(value)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _finite_nonnegative(value: object) -> TypeGuard[int | float]:
|
|
36
|
+
return (
|
|
37
|
+
isinstance(value, (int, float))
|
|
38
|
+
and not isinstance(value, bool)
|
|
39
|
+
and math.isfinite(value)
|
|
40
|
+
and value >= 0
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate_contract(contract):
|
|
45
|
+
if not isinstance(contract, dict):
|
|
46
|
+
raise ValueError("required_evidence must be an object")
|
|
47
|
+
nodes = contract.get("nodes")
|
|
48
|
+
if not isinstance(nodes, list) or not nodes:
|
|
49
|
+
raise ValueError("required_evidence.nodes must be a non-empty array")
|
|
50
|
+
if not isinstance(contract.get("before"), list):
|
|
51
|
+
raise ValueError("required_evidence.before must be an array")
|
|
52
|
+
|
|
53
|
+
normalized = []
|
|
54
|
+
ids = set()
|
|
55
|
+
for index, node in enumerate(nodes, 1):
|
|
56
|
+
if not isinstance(node, dict):
|
|
57
|
+
raise ValueError(f"required evidence node #{index} must be an object")
|
|
58
|
+
node_id = node.get("id")
|
|
59
|
+
if not isinstance(node_id, str) or not node_id.strip() or node_id in ids:
|
|
60
|
+
raise ValueError("required evidence node ids must be unique non-empty strings")
|
|
61
|
+
ids.add(node_id)
|
|
62
|
+
track = node.get("track")
|
|
63
|
+
if track not in {"audio", "video"}:
|
|
64
|
+
raise ValueError(f"required evidence node {node_id} has invalid track")
|
|
65
|
+
content = node.get("content")
|
|
66
|
+
if not isinstance(content, str) or not content.strip():
|
|
67
|
+
raise ValueError(f"required evidence node {node_id} has empty content")
|
|
68
|
+
start, end = node.get("start"), node.get("end")
|
|
69
|
+
if not (_finite_nonnegative(start) and _finite_nonnegative(end) and end > start):
|
|
70
|
+
raise ValueError(f"required evidence node {node_id} has invalid start/end")
|
|
71
|
+
source_id = node.get("source_id")
|
|
72
|
+
if "source_id" in node and (
|
|
73
|
+
not isinstance(source_id, str) or not source_id.strip()
|
|
74
|
+
):
|
|
75
|
+
raise ValueError(f"required evidence node {node_id} has invalid source_id")
|
|
76
|
+
|
|
77
|
+
item = {
|
|
78
|
+
"id": node_id,
|
|
79
|
+
"track": track,
|
|
80
|
+
"content": content,
|
|
81
|
+
"source": _realpath(node.get("source")),
|
|
82
|
+
"start": start,
|
|
83
|
+
"end": end,
|
|
84
|
+
}
|
|
85
|
+
if "source_id" in node:
|
|
86
|
+
item["source_id"] = source_id
|
|
87
|
+
normalized.append(item)
|
|
88
|
+
|
|
89
|
+
edges = []
|
|
90
|
+
for edge in contract["before"]:
|
|
91
|
+
if (
|
|
92
|
+
not isinstance(edge, list)
|
|
93
|
+
or len(edge) != 2
|
|
94
|
+
or edge[0] not in ids
|
|
95
|
+
or edge[1] not in ids
|
|
96
|
+
or edge[0] == edge[1]
|
|
97
|
+
):
|
|
98
|
+
raise ValueError("required_evidence.before contains an invalid node pair")
|
|
99
|
+
edges.append((edge[0], edge[1]))
|
|
100
|
+
return normalized, edges
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _same(value, expected):
|
|
104
|
+
return abs(value - expected) <= _EPSILON
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _matching_segments(node, validated_plan, input_video):
|
|
108
|
+
segments = []
|
|
109
|
+
for clip in validated_plan["clips"]:
|
|
110
|
+
source = _realpath(
|
|
111
|
+
os.fspath(clip.get("source_path", input_video)), require_absolute=False
|
|
112
|
+
)
|
|
113
|
+
if source != node["source"]:
|
|
114
|
+
continue
|
|
115
|
+
if "source_id" in node and clip.get("source_id") != node["source_id"]:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
clip_start = clip["source_start"]
|
|
119
|
+
clip_end = clip["source_end"]
|
|
120
|
+
start = max(node["start"], clip_start)
|
|
121
|
+
end = min(node["end"], clip_end)
|
|
122
|
+
if end <= start:
|
|
123
|
+
continue
|
|
124
|
+
output_start = clip["output_start"] + start - clip_start
|
|
125
|
+
segments.append(
|
|
126
|
+
{
|
|
127
|
+
"source_id": clip.get("source_id"),
|
|
128
|
+
"source_start": start,
|
|
129
|
+
"source_end": end,
|
|
130
|
+
"output_start": output_start,
|
|
131
|
+
"output_end": output_start + end - start,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
return sorted(segments, key=lambda item: item["output_start"])
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _occurrences(node, validated_plan, input_video):
|
|
138
|
+
segments = _matching_segments(node, validated_plan, input_video)
|
|
139
|
+
occurrences = []
|
|
140
|
+
for index, segment in enumerate(segments):
|
|
141
|
+
if not _same(segment["source_start"], node["start"]):
|
|
142
|
+
continue
|
|
143
|
+
source_cursor = segment["source_end"]
|
|
144
|
+
output_cursor = segment["output_end"]
|
|
145
|
+
for following in segments[index + 1 :]:
|
|
146
|
+
if source_cursor >= node["end"] - _EPSILON:
|
|
147
|
+
break
|
|
148
|
+
if (
|
|
149
|
+
following["source_id"] == segment["source_id"]
|
|
150
|
+
and _same(following["source_start"], source_cursor)
|
|
151
|
+
and _same(following["output_start"], output_cursor)
|
|
152
|
+
):
|
|
153
|
+
source_cursor = following["source_end"]
|
|
154
|
+
output_cursor = following["output_end"]
|
|
155
|
+
if _same(source_cursor, node["end"]):
|
|
156
|
+
occurrences.append(
|
|
157
|
+
{"start": segment["output_start"], "end": output_cursor}
|
|
158
|
+
)
|
|
159
|
+
return occurrences
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def check_required_evidence(contract, validated_plan, *, input_video, source_audio) -> dict:
|
|
163
|
+
"""Return bounded source-selection QC for a declared required-evidence contract.
|
|
164
|
+
|
|
165
|
+
`source_audio(realpath) -> bool` is consulted only for audio nodes, after the contract
|
|
166
|
+
has been validated, so callers never pre-walk the raw contract to decide whether to probe."""
|
|
167
|
+
try:
|
|
168
|
+
nodes, edges = _validate_contract(contract)
|
|
169
|
+
except (AttributeError, KeyError, TypeError, ValueError) as exc:
|
|
170
|
+
return _invalid(str(exc))
|
|
171
|
+
|
|
172
|
+
findings = []
|
|
173
|
+
occurrences_by_id = {}
|
|
174
|
+
fragment_starts_by_id = {}
|
|
175
|
+
report_nodes = []
|
|
176
|
+
for node in nodes:
|
|
177
|
+
if node["track"] == "audio" and source_audio(node["source"]) is not True:
|
|
178
|
+
occurrences = []
|
|
179
|
+
fragment_starts = []
|
|
180
|
+
findings.append(
|
|
181
|
+
{
|
|
182
|
+
"code": "REQUIRED_EVIDENCE_AUDIO_UNAVAILABLE",
|
|
183
|
+
"message": (
|
|
184
|
+
f"audio node {node['id']} requires a source with an audio stream"
|
|
185
|
+
),
|
|
186
|
+
"node_id": node["id"],
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
fragment_starts = [
|
|
191
|
+
segment["output_start"]
|
|
192
|
+
for segment in _matching_segments(node, validated_plan, input_video)
|
|
193
|
+
]
|
|
194
|
+
occurrences = _occurrences(node, validated_plan, input_video)
|
|
195
|
+
if not occurrences:
|
|
196
|
+
findings.append(
|
|
197
|
+
{
|
|
198
|
+
"code": "REQUIRED_EVIDENCE_MISSING",
|
|
199
|
+
"message": (
|
|
200
|
+
f"node {node['id']} is not retained as one continuous source "
|
|
201
|
+
"and output span"
|
|
202
|
+
),
|
|
203
|
+
"node_id": node["id"],
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
occurrences_by_id[node["id"]] = occurrences
|
|
207
|
+
fragment_starts_by_id[node["id"]] = fragment_starts
|
|
208
|
+
report_nodes.append({**node, "occurrences": occurrences})
|
|
209
|
+
|
|
210
|
+
for premise_id, result_id in edges:
|
|
211
|
+
premise_occurrences = occurrences_by_id[premise_id]
|
|
212
|
+
result_fragment_starts = fragment_starts_by_id[result_id]
|
|
213
|
+
unpreceded_starts = [
|
|
214
|
+
result_start
|
|
215
|
+
for result_start in result_fragment_starts
|
|
216
|
+
if not any(
|
|
217
|
+
premise["end"] <= result_start + _EPSILON
|
|
218
|
+
for premise in premise_occurrences
|
|
219
|
+
)
|
|
220
|
+
]
|
|
221
|
+
ordered = bool(result_fragment_starts) and not unpreceded_starts
|
|
222
|
+
if not ordered:
|
|
223
|
+
detail = (
|
|
224
|
+
f"; earliest unpreceded output start={min(unpreceded_starts):.9g}s"
|
|
225
|
+
if unpreceded_starts
|
|
226
|
+
else "; no matching result fragment was retained"
|
|
227
|
+
)
|
|
228
|
+
findings.append(
|
|
229
|
+
{
|
|
230
|
+
"code": "REQUIRED_EVIDENCE_ORDER",
|
|
231
|
+
"message": (
|
|
232
|
+
f"every retained fragment of {result_id} must follow a complete "
|
|
233
|
+
f"occurrence of {premise_id}{detail}"
|
|
234
|
+
),
|
|
235
|
+
"node_id": result_id,
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
"selection_status": "BLOCK" if findings else "PASS",
|
|
241
|
+
"nodes": report_nodes,
|
|
242
|
+
"findings": findings,
|
|
243
|
+
"semantic_status": "NOT_CHECKED",
|
|
244
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read-only internal-shot recall on the actual rendered video; never repair an EDL."""
|
|
3
|
+
import argparse
|
|
4
|
+
from bisect import bisect_left
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
from cut_contract import edited_source_render_cache_payload
|
|
15
|
+
from lib import file_identity
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _positive(value, name, *, integer=False):
|
|
19
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
20
|
+
raise ValueError(f"{name} must be a positive number")
|
|
21
|
+
if not math.isfinite(value) or value <= 0 or (integer and not isinstance(value, int)):
|
|
22
|
+
raise ValueError(f"invalid {name}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _validate_clock(pts, end):
|
|
26
|
+
if not pts or any(not isinstance(p, Fraction) for p in [*pts, end]):
|
|
27
|
+
raise ValueError("frame clock requires exact rational PTS")
|
|
28
|
+
if pts[0] != 0 or any(a >= b for a, b in zip(pts, pts[1:])) or end <= pts[-1]:
|
|
29
|
+
raise ValueError("invalid/nonmonotonic frame clock")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def summarize_candidates(pts, end, cut_frames, *, max_short_frames=None,
|
|
33
|
+
max_short_seconds=1.0, dense_window_seconds=2.0,
|
|
34
|
+
min_dense_cuts=4):
|
|
35
|
+
"""Half-open frame spans including the head and tail; thresholds are recall policy."""
|
|
36
|
+
_validate_clock(pts, end)
|
|
37
|
+
_positive(max_short_seconds, "max_short_seconds")
|
|
38
|
+
if max_short_frames is None:
|
|
39
|
+
# A fixed frame count only coincides with the seconds limit at one frame rate, so
|
|
40
|
+
# derive it from the measured clock; --max-short-frames stays an explicit override.
|
|
41
|
+
measured_fps = Fraction(len(pts), 1) / end
|
|
42
|
+
max_short_frames = max(1, round(measured_fps * Fraction(str(max_short_seconds))))
|
|
43
|
+
_positive(max_short_frames, "max_short_frames", integer=True)
|
|
44
|
+
_positive(dense_window_seconds, "dense_window_seconds")
|
|
45
|
+
_positive(min_dense_cuts, "min_dense_cuts", integer=True)
|
|
46
|
+
if any(type(f) is not int or not 0 < f < len(pts) for f in cut_frames):
|
|
47
|
+
raise ValueError("invalid candidate frame index")
|
|
48
|
+
cuts = sorted(set(cut_frames))
|
|
49
|
+
boundaries = [0, *cuts, len(pts)]
|
|
50
|
+
clock = [*pts, end]
|
|
51
|
+
short = []
|
|
52
|
+
for start, stop in zip(boundaries, boundaries[1:]):
|
|
53
|
+
duration = clock[stop] - clock[start]
|
|
54
|
+
if stop - start <= max_short_frames and duration <= Fraction(str(max_short_seconds)):
|
|
55
|
+
short.append({
|
|
56
|
+
"start_frame": start, "end_frame": stop, "frame_count": stop - start,
|
|
57
|
+
"start_exact": str(clock[start]), "end_exact": str(clock[stop]),
|
|
58
|
+
"duration_exact": str(duration), "duration_seconds": float(duration),
|
|
59
|
+
"review_status": "NEEDS_DYNAMIC_REVIEW",
|
|
60
|
+
})
|
|
61
|
+
windows = []
|
|
62
|
+
right = 0
|
|
63
|
+
window_duration = Fraction(str(dense_window_seconds))
|
|
64
|
+
for left in range(len(cuts)):
|
|
65
|
+
right = max(right, left)
|
|
66
|
+
while right < len(cuts) and pts[cuts[right]] - pts[cuts[left]] <= window_duration:
|
|
67
|
+
right += 1
|
|
68
|
+
if right - left < min_dense_cuts:
|
|
69
|
+
continue
|
|
70
|
+
members = cuts[left:right]
|
|
71
|
+
if windows and members[0] <= windows[-1]["cut_frames"][-1]:
|
|
72
|
+
windows[-1]["cut_frames"] = sorted(set(windows[-1]["cut_frames"]) | set(members))
|
|
73
|
+
else:
|
|
74
|
+
windows.append({"cut_frames": members, "review_status": "NEEDS_DYNAMIC_REVIEW"})
|
|
75
|
+
for window in windows:
|
|
76
|
+
window.update({"start_exact": str(pts[window["cut_frames"][0]]),
|
|
77
|
+
"end_exact": str(pts[window["cut_frames"][-1]])})
|
|
78
|
+
return {
|
|
79
|
+
"status": "NEEDS_REVIEW" if short or windows else "NO_CANDIDATES",
|
|
80
|
+
"normal_speed_review": "NOT_CHECKED", "automatic_repairs": [],
|
|
81
|
+
"candidates": [{"frame": f, "pts_exact": str(pts[f]), "origin": "UNKNOWN"} for f in cuts],
|
|
82
|
+
"short_spans": short, "dense_windows": windows,
|
|
83
|
+
"policy": {"max_short_frames": max_short_frames, "max_short_seconds": max_short_seconds,
|
|
84
|
+
"dense_window_seconds": dense_window_seconds, "min_dense_cuts": min_dense_cuts},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def probe_frame_clock(video):
|
|
89
|
+
result = subprocess.run([
|
|
90
|
+
"ffprobe", "-v", "error", "-select_streams", "v:0", "-show_frames",
|
|
91
|
+
"-show_entries", "stream=time_base,start_pts,duration_ts:frame=pts,duration,pkt_duration",
|
|
92
|
+
"-of", "json", str(video),
|
|
93
|
+
], capture_output=True, text=True, timeout=600)
|
|
94
|
+
if result.returncode or result.stderr.strip():
|
|
95
|
+
raise RuntimeError(f"frame decode probe failed: {result.stderr[-2000:]}")
|
|
96
|
+
try:
|
|
97
|
+
data = json.loads(result.stdout)
|
|
98
|
+
stream = data["streams"][0]
|
|
99
|
+
tb = Fraction(stream["time_base"])
|
|
100
|
+
if tb <= 0:
|
|
101
|
+
raise ValueError("nonpositive time base")
|
|
102
|
+
frames = data["frames"]
|
|
103
|
+
absolute = [int(f["pts"]) * tb for f in frames]
|
|
104
|
+
origin = absolute[0]
|
|
105
|
+
pts = [p - origin for p in absolute]
|
|
106
|
+
last_duration = frames[-1].get("duration", frames[-1].get("pkt_duration"))
|
|
107
|
+
if last_duration is None or int(last_duration) <= 0:
|
|
108
|
+
raise ValueError("missing/invalid terminal frame duration")
|
|
109
|
+
decoded_end = pts[-1] + int(last_duration) * tb
|
|
110
|
+
if "duration_ts" in stream:
|
|
111
|
+
end = (int(stream.get("start_pts", frames[0]["pts"])) + int(stream["duration_ts"])) * tb - origin
|
|
112
|
+
if abs(end - decoded_end) > tb:
|
|
113
|
+
raise ValueError("decoded frame coverage does not reach declared stream end")
|
|
114
|
+
else:
|
|
115
|
+
end = decoded_end
|
|
116
|
+
_validate_clock(pts, end)
|
|
117
|
+
except (KeyError, IndexError, ValueError, TypeError, ZeroDivisionError) as exc:
|
|
118
|
+
raise ValueError(f"cannot establish exact frame clock: {exc}") from exc
|
|
119
|
+
return pts, end, origin
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _validate_scene_roi(roi, video):
|
|
123
|
+
"""Validate an ROI (four ints from the CLI) against the video's auto-oriented native frame."""
|
|
124
|
+
if len(roi) != 4:
|
|
125
|
+
raise ValueError("scene ROI must contain exactly four integers")
|
|
126
|
+
x, y, width, height = roi
|
|
127
|
+
if x < 0 or y < 0 or width <= 0 or height <= 0:
|
|
128
|
+
raise ValueError("scene ROI requires x/y >= 0 and width/height > 0")
|
|
129
|
+
|
|
130
|
+
from media_geometry import _probe_video_geometry
|
|
131
|
+
facts = _probe_video_geometry(video).facts
|
|
132
|
+
rotation = facts["rotation"]
|
|
133
|
+
if rotation not in {0, 90, 180, 270}:
|
|
134
|
+
raise ValueError("scene ROI requires a right-angle video rotation")
|
|
135
|
+
canvas_width, canvas_height = (
|
|
136
|
+
(facts["coded_height"], facts["coded_width"]) if rotation in {90, 270}
|
|
137
|
+
else (facts["coded_width"], facts["coded_height"])
|
|
138
|
+
)
|
|
139
|
+
if x + width > canvas_width or y + height > canvas_height:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"scene ROI exceeds auto-oriented native frame {canvas_width}x{canvas_height}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def detect_scene_pts(video, threshold, roi=None):
|
|
146
|
+
"""No seek or float pts_time: showinfo integer pts + its actual filter timebase.
|
|
147
|
+
|
|
148
|
+
The single library-side check of threshold/ROI; the CLIs pre-check with parser.error."""
|
|
149
|
+
if isinstance(threshold, bool) or not isinstance(threshold, (int, float)) or not math.isfinite(threshold) or not 0 <= threshold <= 1:
|
|
150
|
+
raise ValueError("scene threshold must be finite and in [0,1]")
|
|
151
|
+
filters = []
|
|
152
|
+
if roi is not None:
|
|
153
|
+
_validate_scene_roi(roi, video)
|
|
154
|
+
x, y, width, height = roi
|
|
155
|
+
filters.append(f"crop={width}:{height}:{x}:{y}:exact=1")
|
|
156
|
+
filters.extend([f"select='gt(scene,{threshold})'", "showinfo"])
|
|
157
|
+
result = subprocess.run([
|
|
158
|
+
"ffmpeg", "-hide_banner", "-nostdin", "-v", "info", "-xerror", "-copyts",
|
|
159
|
+
"-threads", "2", "-i", str(video), "-map", "0:v:0",
|
|
160
|
+
"-vf", ",".join(filters),
|
|
161
|
+
"-an", "-fps_mode", "passthrough", "-f", "null", "-",
|
|
162
|
+
], capture_output=True, text=True, timeout=600)
|
|
163
|
+
if result.returncode:
|
|
164
|
+
raise RuntimeError(f"scene decode failed: {result.stderr[-2000:]}")
|
|
165
|
+
bases = set(re.findall(r"config in time_base:\s*([0-9]+/[0-9]+)", result.stderr))
|
|
166
|
+
if len(bases) != 1:
|
|
167
|
+
raise ValueError("ambiguous or missing scene filter timebase")
|
|
168
|
+
tb = Fraction(bases.pop())
|
|
169
|
+
if tb <= 0:
|
|
170
|
+
raise ValueError("invalid scene filter timebase")
|
|
171
|
+
values = re.findall(r"\bn:\s*\d+\s+pts:\s*(-?\d+)\s+pts_time:", result.stderr)
|
|
172
|
+
return [int(p) * tb for p in values]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def load_bound_plan(video, plan_path):
|
|
176
|
+
"""Only associate with the current cut-render cache: plan, render settings, sources."""
|
|
177
|
+
from cut_contract import _edited_source_meta_path
|
|
178
|
+
try:
|
|
179
|
+
plan = json.loads(Path(plan_path).read_text(encoding="utf-8"))
|
|
180
|
+
meta = json.loads(_edited_source_meta_path(video).read_text(encoding="utf-8"))
|
|
181
|
+
sources = meta["sources"]
|
|
182
|
+
if not isinstance(sources, dict) or not sources:
|
|
183
|
+
raise ValueError("source identities missing")
|
|
184
|
+
if not Path(video).is_file() or Path(video).stat().st_size == 0:
|
|
185
|
+
raise ValueError("rendered media missing")
|
|
186
|
+
if (meta["plan"] != plan["clips"]
|
|
187
|
+
or meta["render_cache"] != edited_source_render_cache_payload()
|
|
188
|
+
or any(file_identity(p) != identity for p, identity in sources.items())):
|
|
189
|
+
raise ValueError("stale media, plan or render settings")
|
|
190
|
+
declared = {c["source_path"] for c in plan["clips"] if "source_path" in c}
|
|
191
|
+
if declared and declared != set(sources):
|
|
192
|
+
raise ValueError("source set mismatch")
|
|
193
|
+
if not declared and len(sources) != 1:
|
|
194
|
+
raise ValueError("ambiguous single source")
|
|
195
|
+
except (OSError, KeyError, ValueError, TypeError) as exc:
|
|
196
|
+
raise ValueError(f"cut plan binding failed: {exc}") from exc
|
|
197
|
+
return plan
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _associate_plan(report, plan, pts, end):
|
|
201
|
+
"""Legacy seconds are only a coarse locator, not exact source-frame proof."""
|
|
202
|
+
spans = []
|
|
203
|
+
previous = Fraction(0)
|
|
204
|
+
for clip in plan["clips"]:
|
|
205
|
+
start, stop = Fraction(str(clip["output_start"])), Fraction(str(clip["output_end"]))
|
|
206
|
+
if start != previous or stop <= start:
|
|
207
|
+
raise ValueError("bound cut plan must be contiguous and ordered")
|
|
208
|
+
spans.append((start, stop, clip))
|
|
209
|
+
previous = stop
|
|
210
|
+
tolerance = max(b - a for a, b in zip(pts, [*pts[1:], end]))
|
|
211
|
+
if not spans or abs(previous - end) > tolerance:
|
|
212
|
+
raise ValueError("bound plan duration disagrees with actual frame clock")
|
|
213
|
+
joins = [bisect_left(pts, s[0]) for s in spans[1:]]
|
|
214
|
+
for candidate in report["candidates"]:
|
|
215
|
+
f, t = candidate["frame"], pts[candidate["frame"]]
|
|
216
|
+
if any(abs(f - join) <= 1 for join in joins):
|
|
217
|
+
candidate["origin"] = "EDIT_JOIN_CANDIDATE"
|
|
218
|
+
else:
|
|
219
|
+
for start, stop, clip in spans:
|
|
220
|
+
if start < t < stop:
|
|
221
|
+
candidate["inside_bound_clip"] = clip["clip_id"]
|
|
222
|
+
candidate["source_time_estimate"] = str(Fraction(str(clip["source_start"])) + t - start)
|
|
223
|
+
candidate["mapping_precision"] = "legacy_seconds_estimate"
|
|
224
|
+
# No source scan: motion, exposure, overlay animation and native cuts
|
|
225
|
+
# remain indistinguishable from output scene score alone.
|
|
226
|
+
break
|
|
227
|
+
report["plan_joins"] = joins
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def scan_video(video, *, threshold=0.35, plan_path=None, roi=None, **policy):
|
|
231
|
+
video = Path(video).resolve()
|
|
232
|
+
plan = load_bound_plan(video, plan_path) if plan_path is not None else None
|
|
233
|
+
pts, end, origin = probe_frame_clock(video)
|
|
234
|
+
scenes = detect_scene_pts(video, threshold, roi)
|
|
235
|
+
frames_by_pts = {p + origin: i for i, p in enumerate(pts)}
|
|
236
|
+
if any(p not in frames_by_pts for p in scenes):
|
|
237
|
+
raise ValueError("scene candidate does not match a unique decoded frame PTS")
|
|
238
|
+
report = summarize_candidates(pts, end, [frames_by_pts[p] for p in scenes if frames_by_pts[p] > 0], **policy)
|
|
239
|
+
if plan is not None:
|
|
240
|
+
_associate_plan(report, plan, pts, end)
|
|
241
|
+
report.update({
|
|
242
|
+
"schema_version": 1, "artifact": "shot_review", "algorithm": "scene-frame-recall-v1", "scan_complete": True,
|
|
243
|
+
"media": {"path": str(video), "frame_count": len(pts),
|
|
244
|
+
"origin_pts_exact": str(origin), "duration_exact": str(end)},
|
|
245
|
+
"plan_binding": {"path": str(Path(plan_path).resolve())} if plan is not None else None,
|
|
246
|
+
"scene_threshold": threshold,
|
|
247
|
+
"scene_roi": None if roi is None else list(roi),
|
|
248
|
+
"limits": ["scene score is not a confirmed shot or flash-frame defect",
|
|
249
|
+
"source-origin confirmation requires independent source footage review",
|
|
250
|
+
"NO_CANDIDATES is not perceptual approval or listening evidence"],
|
|
251
|
+
})
|
|
252
|
+
return report
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _atomic_json(path, data):
|
|
256
|
+
path = Path(path)
|
|
257
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
258
|
+
temp = None
|
|
259
|
+
try:
|
|
260
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
|
261
|
+
temp = Path(handle.name)
|
|
262
|
+
json.dump(data, handle, ensure_ascii=False, indent=2)
|
|
263
|
+
handle.flush()
|
|
264
|
+
os.fsync(handle.fileno())
|
|
265
|
+
os.replace(temp, path)
|
|
266
|
+
finally:
|
|
267
|
+
if temp is not None:
|
|
268
|
+
temp.unlink(missing_ok=True)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _report_target(video, output, plan_path):
|
|
272
|
+
"""Unknown existing files are never report targets, even with corrupt source metadata."""
|
|
273
|
+
from cut_contract import _edited_source_meta_path
|
|
274
|
+
protected = {Path(video).resolve(), _edited_source_meta_path(video).resolve()}
|
|
275
|
+
if plan_path is not None:
|
|
276
|
+
protected.add(Path(plan_path).resolve())
|
|
277
|
+
target = Path(output).resolve()
|
|
278
|
+
if target in protected:
|
|
279
|
+
raise ValueError("report must not overwrite media, source, plan or render metadata")
|
|
280
|
+
if Path(output).exists():
|
|
281
|
+
try:
|
|
282
|
+
old = json.loads(Path(output).read_text(encoding="utf-8"))
|
|
283
|
+
if (not isinstance(old, dict) or type(old.get("schema_version")) is not int
|
|
284
|
+
or old["schema_version"] != 1 or old.get("artifact") != "shot_review"):
|
|
285
|
+
raise ValueError("not a shot-review artifact")
|
|
286
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
287
|
+
raise ValueError("report must not overwrite an unknown existing file") from exc
|
|
288
|
+
return target
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _protect_declared_sources(video, plan_path, target):
|
|
292
|
+
"""Use both plan and metadata declarations, even when they disagree."""
|
|
293
|
+
from cut_contract import _edited_source_meta_path
|
|
294
|
+
if plan_path is None:
|
|
295
|
+
return
|
|
296
|
+
paths, errors = set(), []
|
|
297
|
+
for path, kind in [(Path(plan_path), "plan"), (_edited_source_meta_path(video), "meta")]:
|
|
298
|
+
try:
|
|
299
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
300
|
+
if kind == "plan":
|
|
301
|
+
paths.update(c["source_path"] for c in data["clips"] if "source_path" in c)
|
|
302
|
+
else:
|
|
303
|
+
paths.update(data["sources"])
|
|
304
|
+
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
305
|
+
errors.append(exc)
|
|
306
|
+
if target in {Path(p).resolve() for p in paths}:
|
|
307
|
+
raise UnsafeReportTarget("report must not overwrite a declared source")
|
|
308
|
+
if errors:
|
|
309
|
+
raise ValueError(f"cannot verify declared source protection: {errors[0]}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class UnsafeReportTarget(ValueError):
|
|
313
|
+
"""Do not write even failure evidence to an input file."""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def write_scan(video, output, **options):
|
|
317
|
+
target = _report_target(video, output, options.get("plan_path"))
|
|
318
|
+
roi = options.get("roi")
|
|
319
|
+
base = {"schema_version": 1, "artifact": "shot_review", "scan_complete": False,
|
|
320
|
+
"normal_speed_review": "NOT_CHECKED",
|
|
321
|
+
"scene_threshold": options.get("threshold", 0.35),
|
|
322
|
+
"scene_roi": None if roi is None else list(roi)}
|
|
323
|
+
try:
|
|
324
|
+
_protect_declared_sources(video, options.get("plan_path"), target)
|
|
325
|
+
_atomic_json(output, {**base, "status": "SCANNING"})
|
|
326
|
+
report = scan_video(video, **options)
|
|
327
|
+
except UnsafeReportTarget:
|
|
328
|
+
raise
|
|
329
|
+
except (OSError, ValueError, RuntimeError, KeyError, TypeError, subprocess.SubprocessError) as exc:
|
|
330
|
+
_atomic_json(output, {**base, "status": "SCAN_FAILED", "error": str(exc)})
|
|
331
|
+
raise
|
|
332
|
+
_atomic_json(output, report)
|
|
333
|
+
return report
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main():
|
|
337
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
338
|
+
parser.add_argument("video")
|
|
339
|
+
parser.add_argument("--output", required=True)
|
|
340
|
+
parser.add_argument("--plan", default=None, help="optional current clip_plan_validated.json; stale bindings fail")
|
|
341
|
+
parser.add_argument("--threshold", type=float, default=0.35)
|
|
342
|
+
parser.add_argument("--roi", nargs=4, type=int, metavar=("X", "Y", "WIDTH", "HEIGHT"))
|
|
343
|
+
parser.add_argument("--max-short-frames", type=int, default=None,
|
|
344
|
+
help="explicit frame cap; default derives round(fps * max_short_seconds)")
|
|
345
|
+
parser.add_argument("--max-short-seconds", type=float, default=1.0)
|
|
346
|
+
parser.add_argument("--dense-window-seconds", type=float, default=2.0)
|
|
347
|
+
parser.add_argument("--min-dense-cuts", type=int, default=4)
|
|
348
|
+
args = parser.parse_args()
|
|
349
|
+
report = write_scan(args.video, args.output, threshold=args.threshold, plan_path=args.plan, roi=args.roi,
|
|
350
|
+
max_short_frames=args.max_short_frames, max_short_seconds=args.max_short_seconds,
|
|
351
|
+
dense_window_seconds=args.dense_window_seconds, min_dense_cuts=args.min_dense_cuts)
|
|
352
|
+
print(json.dumps({"status": report["status"], "short_spans": len(report["short_spans"]),
|
|
353
|
+
"dense_windows": len(report["dense_windows"]), "report": args.output}))
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
if __name__ == "__main__":
|
|
357
|
+
main()
|