@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,187 @@
|
|
|
1
|
+
"""Probe and verify an adopted AAC stream without decoding or rewriting it."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from fractions import Fraction
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from lib import run_cmd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _fraction(value):
|
|
11
|
+
return Fraction(str(value))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _packet_span(audio):
|
|
15
|
+
"""The stream duration proved by its packets: last packet end minus first packet start."""
|
|
16
|
+
packets = audio["packets"]
|
|
17
|
+
if not packets or packets[0]["pts"] is None or packets[-1]["pts"] is None \
|
|
18
|
+
or packets[-1]["duration"] is None:
|
|
19
|
+
return None
|
|
20
|
+
return Fraction(packets[-1]["pts"]) + Fraction(packets[-1]["duration"]) \
|
|
21
|
+
- Fraction(packets[0]["pts"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _rational_ticks(value, time_base):
|
|
25
|
+
if value in (None, "N/A"):
|
|
26
|
+
return None
|
|
27
|
+
value = Fraction(int(value)) * Fraction(time_base)
|
|
28
|
+
return f"{value.numerator}/{value.denominator}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _probe(path, *args):
|
|
32
|
+
result = run_cmd([
|
|
33
|
+
"ffprobe", "-v", "error", *args, "-of", "json", str(path),
|
|
34
|
+
])
|
|
35
|
+
if result.returncode != 0:
|
|
36
|
+
raise RuntimeError(f"无法探测媒体 {path}: {result.stderr.strip()}")
|
|
37
|
+
try:
|
|
38
|
+
return json.loads(result.stdout)
|
|
39
|
+
except (TypeError, ValueError) as exc:
|
|
40
|
+
raise RuntimeError(f"ffprobe 返回无效 JSON: {path}") from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def probe_audio_packets(path, audio_stream_index):
|
|
44
|
+
"""Return packet sizes and rational timestamps for one audio stream.
|
|
45
|
+
|
|
46
|
+
``audio_stream_index`` is the zero-based audio-stream ordinal accepted by
|
|
47
|
+
ffmpeg's ``0:a:N`` selector, not the file-wide absolute stream index.
|
|
48
|
+
"""
|
|
49
|
+
payload = _probe(
|
|
50
|
+
path,
|
|
51
|
+
"-select_streams", f"a:{audio_stream_index}",
|
|
52
|
+
"-show_streams", "-show_packets",
|
|
53
|
+
"-show_entries",
|
|
54
|
+
"stream=index,codec_name,time_base,start_pts,start_time,duration_ts,duration,sample_rate,channels,"
|
|
55
|
+
"channel_layout:packet=pts,dts,duration,size,side_data_list",
|
|
56
|
+
)
|
|
57
|
+
streams = payload.get("streams", [])
|
|
58
|
+
if len(streams) != 1:
|
|
59
|
+
raise RuntimeError(f"找不到音频流 a:{audio_stream_index}: {Path(path)}")
|
|
60
|
+
stream = streams[0]
|
|
61
|
+
time_base = stream.get("time_base")
|
|
62
|
+
if not time_base:
|
|
63
|
+
raise RuntimeError(f"音频流 a:{audio_stream_index} 缺少 time_base")
|
|
64
|
+
codec = stream.get("codec_name")
|
|
65
|
+
sample_rate = int(stream["sample_rate"]) if stream.get("sample_rate") else None
|
|
66
|
+
channels = int(stream["channels"]) if stream.get("channels") else None
|
|
67
|
+
channel_layout = stream.get("channel_layout")
|
|
68
|
+
decoder = {
|
|
69
|
+
"codec": codec,
|
|
70
|
+
"sample_rate": sample_rate,
|
|
71
|
+
"channels": channels,
|
|
72
|
+
"channel_layout": channel_layout,
|
|
73
|
+
}
|
|
74
|
+
packets = []
|
|
75
|
+
for packet in payload.get("packets", []):
|
|
76
|
+
if packet.get("size") is None:
|
|
77
|
+
raise RuntimeError(f"音频流 a:{audio_stream_index} 的 packet 缺少 size")
|
|
78
|
+
packets.append({
|
|
79
|
+
"size": int(packet["size"]),
|
|
80
|
+
"pts": _rational_ticks(packet.get("pts"), time_base),
|
|
81
|
+
"dts": _rational_ticks(packet.get("dts"), time_base),
|
|
82
|
+
"duration": _rational_ticks(packet.get("duration"), time_base),
|
|
83
|
+
"side_data_list": packet.get("side_data_list", []),
|
|
84
|
+
})
|
|
85
|
+
return {
|
|
86
|
+
"selected_audio_stream_index": audio_stream_index,
|
|
87
|
+
"absolute_stream_index": int(stream["index"]),
|
|
88
|
+
"codec": codec,
|
|
89
|
+
"time_base": time_base,
|
|
90
|
+
"start_time": stream.get("start_time"),
|
|
91
|
+
"duration": stream.get("duration"),
|
|
92
|
+
"sample_rate": sample_rate,
|
|
93
|
+
"channels": channels,
|
|
94
|
+
"channel_layout": channel_layout,
|
|
95
|
+
"decoder": decoder,
|
|
96
|
+
"packet_count": len(packets),
|
|
97
|
+
"payload_bytes": sum(packet["size"] for packet in packets),
|
|
98
|
+
"packets": packets,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _picture_interval(path):
|
|
103
|
+
payload = _probe(
|
|
104
|
+
path,
|
|
105
|
+
"-select_streams", "v:0", "-show_streams",
|
|
106
|
+
"-show_entries", "stream=time_base,start_pts,start_time,duration_ts,duration,avg_frame_rate",
|
|
107
|
+
)
|
|
108
|
+
streams = payload.get("streams", [])
|
|
109
|
+
if len(streams) != 1:
|
|
110
|
+
raise RuntimeError(f"找不到主画面流 v:0: {Path(path)}")
|
|
111
|
+
stream = streams[0]
|
|
112
|
+
start = _fraction(stream.get("start_time", "0"))
|
|
113
|
+
if stream.get("duration") in (None, "N/A"):
|
|
114
|
+
raise RuntimeError("主画面流缺少可验证时长")
|
|
115
|
+
duration = _fraction(stream["duration"])
|
|
116
|
+
frame_rate = Fraction(stream.get("avg_frame_rate", "0/1"))
|
|
117
|
+
frame = Fraction(1, 1000) if frame_rate <= 0 else 1 / frame_rate
|
|
118
|
+
return start, duration, frame
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validate_adopted_source(path, audio_stream_index):
|
|
122
|
+
"""Fail unless the selected input is copyable AAC spanning the picture interval."""
|
|
123
|
+
audio = probe_audio_packets(path, audio_stream_index)
|
|
124
|
+
if audio["codec"] != "aac":
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"adopted-packet-copy 当前只支持 MP4 AAC stream-copy;"
|
|
127
|
+
f"a:{audio_stream_index} 是 {audio['codec'] or 'unknown'}"
|
|
128
|
+
)
|
|
129
|
+
if not audio["packets"]:
|
|
130
|
+
raise RuntimeError(f"音频流 a:{audio_stream_index} 没有 packet,不能采用")
|
|
131
|
+
picture_start, picture_duration, frame_tolerance = _picture_interval(path)
|
|
132
|
+
if audio["start_time"] in (None, "N/A") or audio["duration"] in (None, "N/A"):
|
|
133
|
+
raise RuntimeError(f"音频流 a:{audio_stream_index} 缺少可验证起止时间")
|
|
134
|
+
audio_start = _fraction(audio["start_time"])
|
|
135
|
+
audio_duration = _fraction(audio["duration"])
|
|
136
|
+
packet_durations = [
|
|
137
|
+
_fraction(packet["duration"])
|
|
138
|
+
for packet in audio["packets"] if packet["duration"] is not None
|
|
139
|
+
]
|
|
140
|
+
tolerance = max([frame_tolerance, Fraction(1, 1000), *packet_durations])
|
|
141
|
+
if (
|
|
142
|
+
abs(audio_start - picture_start) > tolerance
|
|
143
|
+
or abs(audio_duration - picture_duration) > tolerance
|
|
144
|
+
):
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
"采用音频与画面时长/起点不兼容: "
|
|
147
|
+
f"picture={float(picture_start):.6f}+{float(picture_duration):.6f}s, "
|
|
148
|
+
f"audio={float(audio_start):.6f}+{float(audio_duration):.6f}s"
|
|
149
|
+
)
|
|
150
|
+
return audio
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def verify_adopted_audio(input_path, output_path, input_stream_index, output_stream_index=0):
|
|
154
|
+
"""Probe the rendered output and check packet count, payload bytes, duration and timing."""
|
|
155
|
+
expected = probe_audio_packets(input_path, input_stream_index)
|
|
156
|
+
actual = probe_audio_packets(output_path, output_stream_index)
|
|
157
|
+
if expected["decoder"] != actual["decoder"]:
|
|
158
|
+
raise RuntimeError("采用音频 decoder 参数已改变")
|
|
159
|
+
if expected["packet_count"] != actual["packet_count"]:
|
|
160
|
+
raise RuntimeError(
|
|
161
|
+
f"采用音频 packet count 已改变: {expected['packet_count']} -> {actual['packet_count']}"
|
|
162
|
+
)
|
|
163
|
+
if expected["payload_bytes"] != actual["payload_bytes"]:
|
|
164
|
+
raise RuntimeError("采用音频 packet payload 总字节数不一致")
|
|
165
|
+
if _packet_span(expected) != _packet_span(actual):
|
|
166
|
+
raise RuntimeError("采用音频流时长已改变")
|
|
167
|
+
expected_packet_core = [
|
|
168
|
+
{key: value for key, value in packet.items() if key != "side_data_list"}
|
|
169
|
+
for packet in expected["packets"]
|
|
170
|
+
]
|
|
171
|
+
actual_packet_core = [
|
|
172
|
+
{key: value for key, value in packet.items() if key != "side_data_list"}
|
|
173
|
+
for packet in actual["packets"]
|
|
174
|
+
]
|
|
175
|
+
if expected_packet_core != actual_packet_core:
|
|
176
|
+
raise RuntimeError("采用音频 packet PTS/DTS/duration/size 不一致")
|
|
177
|
+
if [packet["side_data_list"] for packet in expected["packets"]] != [
|
|
178
|
+
packet["side_data_list"] for packet in actual["packets"]
|
|
179
|
+
]:
|
|
180
|
+
raise RuntimeError("采用音频 packet side data 已改变")
|
|
181
|
+
return {
|
|
182
|
+
"verified": True,
|
|
183
|
+
"selected_audio_stream_index": input_stream_index,
|
|
184
|
+
"output_audio_stream_index": output_stream_index,
|
|
185
|
+
"input": expected,
|
|
186
|
+
"output": actual,
|
|
187
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Record which narration inputs an assembly consumed: adoption, snapshot, placement, output."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from adoption.frozen_audio import probe_audio_packets
|
|
10
|
+
from adoption.strict_inputs import (
|
|
11
|
+
read_json_bytes, require_fields, require_local_path, without_digests, write_json_atomic,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ARTIFACT = "narration_input_binding"
|
|
16
|
+
FILENAME = "narration_input_binding.json"
|
|
17
|
+
IDENTITY_STATUSES = frozenset({"UNADOPTED", "BOUND_TO_ADOPTION"})
|
|
18
|
+
# The conservative default an adoption gets when it declares nothing stronger.
|
|
19
|
+
TEMPO_POLICY = {
|
|
20
|
+
"global_atempo": 1.0,
|
|
21
|
+
"bounded_segment_fit": False,
|
|
22
|
+
"segment_tempo_max": 1.0,
|
|
23
|
+
"cumulative_tempo_max": 1.0,
|
|
24
|
+
"cumulative_tempo_hard_max": 1.0,
|
|
25
|
+
}
|
|
26
|
+
TEMPO_NUMBERS = ("global_atempo", "segment_tempo_max", "cumulative_tempo_max",
|
|
27
|
+
"cumulative_tempo_hard_max")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_tempo_policy(value):
|
|
31
|
+
"""Accept any adoption-declared tempo policy whose shape and bounds hold.
|
|
32
|
+
|
|
33
|
+
The adoption, not this module, decides how fast its own narration may be
|
|
34
|
+
played; the module only refuses policies that are malformed or that would
|
|
35
|
+
let a segment exceed the cumulative hard ceiling the policy itself declares.
|
|
36
|
+
"""
|
|
37
|
+
require_fields(value, sorted(TEMPO_POLICY), "tempo_policy")
|
|
38
|
+
policy = {}
|
|
39
|
+
for key in TEMPO_NUMBERS:
|
|
40
|
+
number = value[key]
|
|
41
|
+
if type(number) not in (int, float) or not math.isfinite(number):
|
|
42
|
+
raise ValueError(f"tempo_policy {key} must be a finite number")
|
|
43
|
+
policy[key] = float(number)
|
|
44
|
+
if type(value["bounded_segment_fit"]) is not bool:
|
|
45
|
+
raise ValueError("tempo_policy bounded_segment_fit must be a boolean")
|
|
46
|
+
policy["bounded_segment_fit"] = value["bounded_segment_fit"]
|
|
47
|
+
if policy["cumulative_tempo_max"] < 1.0:
|
|
48
|
+
raise ValueError("tempo_policy cumulative_tempo_max must be at least 1.0")
|
|
49
|
+
if policy["cumulative_tempo_hard_max"] < policy["cumulative_tempo_max"]:
|
|
50
|
+
raise ValueError("tempo_policy cumulative_tempo_hard_max must not be below "
|
|
51
|
+
"cumulative_tempo_max")
|
|
52
|
+
if policy["segment_tempo_max"] < 1.0:
|
|
53
|
+
raise ValueError("tempo_policy segment_tempo_max must be at least 1.0")
|
|
54
|
+
if not 0 < policy["global_atempo"] <= policy["cumulative_tempo_hard_max"]:
|
|
55
|
+
raise ValueError("tempo_policy global_atempo must be positive and within "
|
|
56
|
+
"cumulative_tempo_hard_max")
|
|
57
|
+
return policy
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def load_adoption(path, *, tts_meta_path, tts_segments):
|
|
61
|
+
"""Load a strict v1 adoption and check it against the current tts_meta segments."""
|
|
62
|
+
if tts_meta_path is None:
|
|
63
|
+
raise ValueError("narration adoption requires explicit tts_meta_path")
|
|
64
|
+
adoption_path, _, adoption = read_json_bytes(path, "narration adoption")
|
|
65
|
+
adoption = without_digests(adoption, "narration adoption")
|
|
66
|
+
require_fields(adoption, ["artifact", "schema_version", "segments", "tempo_policy"],
|
|
67
|
+
"narration adoption")
|
|
68
|
+
if adoption["artifact"] != "narration_adoption" or type(adoption["schema_version"]) is not int \
|
|
69
|
+
or adoption["schema_version"] != 1:
|
|
70
|
+
raise ValueError("unsupported narration_adoption schema")
|
|
71
|
+
tts_meta_path, _, tts_meta = read_json_bytes(tts_meta_path, "tts_meta")
|
|
72
|
+
if not isinstance(tts_meta, dict) or not isinstance(tts_meta.get("segments"), list):
|
|
73
|
+
raise ValueError("tts_meta requires a segments list")
|
|
74
|
+
if tts_meta["segments"] != tts_segments:
|
|
75
|
+
raise ValueError("in-memory narration segments differ from bound tts_meta")
|
|
76
|
+
tempo_policy = validate_tempo_policy(adoption["tempo_policy"])
|
|
77
|
+
if not isinstance(adoption["segments"], list) or len(adoption["segments"]) != len(tts_segments):
|
|
78
|
+
raise ValueError("adoption segments must exactly cover tts_meta segments")
|
|
79
|
+
normalized_segments = []
|
|
80
|
+
for adopted, actual in zip(adoption["segments"], tts_segments):
|
|
81
|
+
adopted = without_digests(adopted, "adoption segment")
|
|
82
|
+
require_fields(adopted, ["index", "spoken_text", "requested_provider", "requested_voice"],
|
|
83
|
+
"adoption segment")
|
|
84
|
+
if type(adopted["index"]) is not int or adopted["index"] != actual.get("index"):
|
|
85
|
+
raise ValueError("adoption segment index/order differs from tts_meta")
|
|
86
|
+
spoken = actual.get("spoken_text", actual.get("narration"))
|
|
87
|
+
if not isinstance(adopted["spoken_text"], str) or adopted["spoken_text"] != spoken:
|
|
88
|
+
raise ValueError("adoption spoken_text differs from tts_meta")
|
|
89
|
+
for key in ("requested_provider", "requested_voice"):
|
|
90
|
+
if not isinstance(adopted[key], str) or not adopted[key]:
|
|
91
|
+
raise ValueError(f"adoption {key} must be a non-empty string")
|
|
92
|
+
normalized_segments.append(dict(adopted))
|
|
93
|
+
return {
|
|
94
|
+
"path": str(adoption_path),
|
|
95
|
+
"tts_meta": {"path": str(tts_meta_path)},
|
|
96
|
+
"segments": normalized_segments, "tempo_policy": tempo_policy,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _copy_snapshot(source, destination):
|
|
101
|
+
"""Copy seam kept small so tests can inject a post-preflight source mutation."""
|
|
102
|
+
shutil.copyfile(source, destination)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def prepare_binding(tts_segments, work_dir, *, narration_adoption_path=None,
|
|
106
|
+
tts_meta_path=None):
|
|
107
|
+
"""Validate first, then snapshot the adopted narration inputs into work_dir."""
|
|
108
|
+
if not isinstance(tts_segments, list):
|
|
109
|
+
raise ValueError("tts_segments must be a list")
|
|
110
|
+
adoption = (
|
|
111
|
+
load_adoption(narration_adoption_path, tts_meta_path=tts_meta_path,
|
|
112
|
+
tts_segments=tts_segments)
|
|
113
|
+
if narration_adoption_path is not None else None
|
|
114
|
+
)
|
|
115
|
+
if not adoption:
|
|
116
|
+
originals = [
|
|
117
|
+
{"index": segment["index"], "source": Path(segment["audio_path"]).resolve(),
|
|
118
|
+
"spoken_text": segment["spoken_text"]}
|
|
119
|
+
for segment in tts_segments
|
|
120
|
+
]
|
|
121
|
+
return {
|
|
122
|
+
"identity_status": "UNADOPTED", "adoption": None, "tempo_policy": None,
|
|
123
|
+
"originals": originals, "active": False, "segments": [],
|
|
124
|
+
}
|
|
125
|
+
originals = []
|
|
126
|
+
seen_indices = set()
|
|
127
|
+
for position, segment in enumerate(tts_segments):
|
|
128
|
+
if not isinstance(segment, dict) or type(segment.get("index")) is not int:
|
|
129
|
+
raise ValueError("each narration segment requires an integer index")
|
|
130
|
+
if segment["index"] in seen_indices:
|
|
131
|
+
raise ValueError("narration segment indices must be unique")
|
|
132
|
+
seen_indices.add(segment["index"])
|
|
133
|
+
adopted = adoption["segments"][position]
|
|
134
|
+
source = require_local_path(segment["audio_path"], "narration audio")
|
|
135
|
+
originals.append({
|
|
136
|
+
"index": segment["index"], "source": source,
|
|
137
|
+
"spoken_text": segment["spoken_text"],
|
|
138
|
+
"requested_provider": adopted["requested_provider"],
|
|
139
|
+
"requested_voice": adopted["requested_voice"],
|
|
140
|
+
})
|
|
141
|
+
context = {
|
|
142
|
+
"identity_status": "BOUND_TO_ADOPTION", "adoption": adoption,
|
|
143
|
+
"tempo_policy": adoption["tempo_policy"],
|
|
144
|
+
"originals": originals, "active": True, "segments": [], "sealed": None,
|
|
145
|
+
}
|
|
146
|
+
snapshot_dir = Path(work_dir).resolve() / ".narration_input_snapshots"
|
|
147
|
+
snapshot_dir.mkdir(parents=True, exist_ok=False)
|
|
148
|
+
for position, (segment, original) in enumerate(zip(tts_segments, originals)):
|
|
149
|
+
suffix = original["source"].suffix or ".audio"
|
|
150
|
+
snapshot = snapshot_dir / f"segment_{position:04d}_{segment['index']}{suffix}"
|
|
151
|
+
_copy_snapshot(original["source"], snapshot)
|
|
152
|
+
segment["audio_path"] = str(snapshot)
|
|
153
|
+
segment["narration_input_original"] = {"path": str(original["source"])}
|
|
154
|
+
segment["narration_input_snapshot"] = {"path": str(snapshot)}
|
|
155
|
+
context["segments"].append({
|
|
156
|
+
"index": original["index"], "spoken_text": original["spoken_text"],
|
|
157
|
+
"requested_provider": original["requested_provider"],
|
|
158
|
+
"requested_voice": original["requested_voice"],
|
|
159
|
+
"original": dict(segment["narration_input_original"]),
|
|
160
|
+
"snapshot": dict(segment["narration_input_snapshot"]),
|
|
161
|
+
})
|
|
162
|
+
return context
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _pcm(path):
|
|
166
|
+
result = subprocess.run(
|
|
167
|
+
["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_streams",
|
|
168
|
+
"-show_entries", "stream=codec_name,sample_fmt,sample_rate,channels,channel_layout",
|
|
169
|
+
"-of", "json", str(path)], capture_output=True, text=True, timeout=600,
|
|
170
|
+
)
|
|
171
|
+
if result.returncode:
|
|
172
|
+
raise ValueError(f"audio probe failed: {result.stderr.strip()}")
|
|
173
|
+
streams = json.loads(result.stdout).get("streams", [])
|
|
174
|
+
if len(streams) != 1:
|
|
175
|
+
raise ValueError("expected exactly one audio stream")
|
|
176
|
+
stream = streams[0]
|
|
177
|
+
return {key: stream.get(key) for key in (
|
|
178
|
+
"codec_name", "sample_fmt", "sample_rate", "channels", "channel_layout"
|
|
179
|
+
)}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _asset(path, *, pcm=False):
|
|
183
|
+
path = require_local_path(path, "binding asset")
|
|
184
|
+
value = {"path": str(path)}
|
|
185
|
+
if pcm:
|
|
186
|
+
value["pcm"] = _pcm(path)
|
|
187
|
+
return value
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def seal_render_inputs(context, tts_segments, narration_wav):
|
|
191
|
+
"""Record every derived audio file that the final FFmpeg command will consume."""
|
|
192
|
+
if not context.get("active"):
|
|
193
|
+
return None
|
|
194
|
+
by_index = {segment["index"]: segment for segment in tts_segments}
|
|
195
|
+
sealed_segments = []
|
|
196
|
+
for item in context["segments"]:
|
|
197
|
+
segment = by_index[item["index"]]
|
|
198
|
+
placed = segment.get("placed_audio_path")
|
|
199
|
+
if not placed:
|
|
200
|
+
raise RuntimeError("active narration binding has no complete placed audio")
|
|
201
|
+
conversion = segment.get("narration_conversion_path")
|
|
202
|
+
conversion_asset = (
|
|
203
|
+
{"applied": True, **_asset(conversion, pcm=True)}
|
|
204
|
+
if conversion else {"applied": False}
|
|
205
|
+
)
|
|
206
|
+
sealed_segments.append({
|
|
207
|
+
"index": item["index"],
|
|
208
|
+
"conversion": conversion_asset,
|
|
209
|
+
"placed": _asset(placed, pcm=True),
|
|
210
|
+
})
|
|
211
|
+
context["sealed"] = {
|
|
212
|
+
"segments": sealed_segments,
|
|
213
|
+
"narration_bus": _asset(narration_wav, pcm=True),
|
|
214
|
+
}
|
|
215
|
+
return context["sealed"]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _active_report(context, rendered_output, final_output):
|
|
219
|
+
if not context.get("sealed"):
|
|
220
|
+
raise RuntimeError("active narration inputs must be sealed before finalization")
|
|
221
|
+
sealed = {item["index"]: item for item in context["sealed"]["segments"]}
|
|
222
|
+
segments = []
|
|
223
|
+
for item in context["segments"]:
|
|
224
|
+
segments.append({
|
|
225
|
+
**item,
|
|
226
|
+
"original": _asset(item["original"]["path"], pcm=True),
|
|
227
|
+
"snapshot": _asset(item["snapshot"]["path"], pcm=True),
|
|
228
|
+
"conversion": sealed[item["index"]]["conversion"],
|
|
229
|
+
"placed": sealed[item["index"]]["placed"],
|
|
230
|
+
})
|
|
231
|
+
rendered_path = require_local_path(rendered_output, "rendered output")
|
|
232
|
+
final_audio = probe_audio_packets(rendered_path, 0)
|
|
233
|
+
return {
|
|
234
|
+
"artifact": ARTIFACT, "schema_version": 1, "status": "FINALIZED",
|
|
235
|
+
"identity_status": context["identity_status"], "adoption": context["adoption"],
|
|
236
|
+
"segments": segments, "narration_bus": context["sealed"]["narration_bus"],
|
|
237
|
+
"final_output": {
|
|
238
|
+
"path": str(Path(final_output).resolve()),
|
|
239
|
+
"audio_stream": {
|
|
240
|
+
"decoder": final_audio["decoder"], "packet_count": final_audio["packet_count"],
|
|
241
|
+
"payload_bytes": final_audio["payload_bytes"],
|
|
242
|
+
"start_time": final_audio["start_time"], "duration": final_audio["duration"],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
"voice_authentication": "NOT_CHECKED", "direct_listening": "NOT_CHECKED",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def finalize_binding(context, tts_segments, narration_wav, final_output, *, rendered_output=None):
|
|
250
|
+
"""Write ``narration_input_binding.json`` beside ``narration_wav`` and return it.
|
|
251
|
+
|
|
252
|
+
``rendered_output`` is the candidate file to probe when the published
|
|
253
|
+
``final_output`` path does not exist yet; it defaults to ``final_output``.
|
|
254
|
+
"""
|
|
255
|
+
del tts_segments # already sealed; the record describes what was consumed
|
|
256
|
+
if not context.get("active"):
|
|
257
|
+
report = {
|
|
258
|
+
"artifact": ARTIFACT, "schema_version": 1, "status": "FINALIZED",
|
|
259
|
+
"identity_status": "UNADOPTED", "adoption": None,
|
|
260
|
+
"segments": [
|
|
261
|
+
{
|
|
262
|
+
"index": original["index"], "spoken_text": original["spoken_text"],
|
|
263
|
+
"requested_provider": None, "requested_voice": None,
|
|
264
|
+
"original": {"path": str(original["source"])},
|
|
265
|
+
"snapshot": None, "conversion": {"applied": False},
|
|
266
|
+
"placed": None,
|
|
267
|
+
}
|
|
268
|
+
for original in context["originals"]
|
|
269
|
+
],
|
|
270
|
+
"narration_bus": ({"path": str(Path(narration_wav).resolve())}
|
|
271
|
+
if Path(narration_wav).is_file() else None),
|
|
272
|
+
"final_output": ({"path": str(Path(final_output).resolve())}
|
|
273
|
+
if Path(final_output).is_file() else None),
|
|
274
|
+
"voice_authentication": "NOT_CHECKED", "direct_listening": "NOT_CHECKED",
|
|
275
|
+
}
|
|
276
|
+
else:
|
|
277
|
+
report = _active_report(
|
|
278
|
+
context, final_output if rendered_output is None else rendered_output, final_output
|
|
279
|
+
)
|
|
280
|
+
write_json_atomic(Path(narration_wav).resolve().parent / FILENAME, report)
|
|
281
|
+
return report
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def record_of(report, path):
|
|
285
|
+
"""The manifest/QC summary of one finalized binding report stored at ``path``."""
|
|
286
|
+
return {"path": str(Path(path).resolve()),
|
|
287
|
+
"identity_status": report["identity_status"],
|
|
288
|
+
"tempo_policy": (
|
|
289
|
+
report["adoption"]["tempo_policy"]
|
|
290
|
+
if report["identity_status"] == "BOUND_TO_ADOPTION" else None
|
|
291
|
+
)}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def binding_record(work_dir):
|
|
295
|
+
"""The finalized binding's path, identity status and tempo policy; None when absent/invalid."""
|
|
296
|
+
path = Path(work_dir) / FILENAME
|
|
297
|
+
if not path.is_file():
|
|
298
|
+
return None
|
|
299
|
+
try:
|
|
300
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
301
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
302
|
+
return None
|
|
303
|
+
if (
|
|
304
|
+
not isinstance(value, dict)
|
|
305
|
+
or value.get("artifact") != ARTIFACT
|
|
306
|
+
or type(value.get("schema_version")) is not int
|
|
307
|
+
or value.get("schema_version") != 1
|
|
308
|
+
or value.get("status") != "FINALIZED"
|
|
309
|
+
or value.get("identity_status") not in IDENTITY_STATUSES
|
|
310
|
+
):
|
|
311
|
+
return None
|
|
312
|
+
if value["identity_status"] == "BOUND_TO_ADOPTION":
|
|
313
|
+
adoption = value.get("adoption")
|
|
314
|
+
if not isinstance(adoption, dict):
|
|
315
|
+
return None
|
|
316
|
+
try:
|
|
317
|
+
validate_tempo_policy(adoption.get("tempo_policy"))
|
|
318
|
+
except ValueError:
|
|
319
|
+
return None
|
|
320
|
+
final_output = value.get("final_output")
|
|
321
|
+
if not isinstance(final_output, dict) or not Path(str(final_output.get("path", ""))).is_file():
|
|
322
|
+
return None
|
|
323
|
+
return record_of(value, path)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Shared strict-input helpers for the explicit-input assemble modules.
|
|
2
|
+
|
|
3
|
+
Every helper here fails closed with ``ValueError`` on malformed input and never
|
|
4
|
+
guesses: a path must be a local file, a JSON document must parse, a rational must
|
|
5
|
+
be canonical ``N/D``. ``run_logged`` and ``probe_json`` wrap the external tools so
|
|
6
|
+
each caller logs the same evidence.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from fractions import Fraction
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def require_fields(value, required, label):
|
|
17
|
+
if not isinstance(value, dict) or set(value) != set(required):
|
|
18
|
+
raise ValueError(f"{label} requires exactly fields {required}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def without_digests(value, label):
|
|
22
|
+
"""Drop the ``sha256`` / ``*_sha256`` keys older caller JSON declared; they are ignored."""
|
|
23
|
+
if not isinstance(value, dict):
|
|
24
|
+
raise ValueError(f"{label} must be an object")
|
|
25
|
+
return {key: item for key, item in value.items()
|
|
26
|
+
if key != "sha256" and not key.endswith("_sha256")}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def require_integer(value, label, minimum=0):
|
|
30
|
+
if type(value) is not int or value < minimum:
|
|
31
|
+
raise ValueError(f"{label} must be an integer >= {minimum}")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def require_number(value, label, minimum, maximum):
|
|
36
|
+
if type(value) not in (int, float) or not math.isfinite(value) \
|
|
37
|
+
or not minimum <= value <= maximum:
|
|
38
|
+
raise ValueError(f"{label} must be finite in [{minimum},{maximum}]")
|
|
39
|
+
return float(value)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def require_local_path(path, label):
|
|
43
|
+
if not isinstance(path, (str, Path)) or not str(path) or "://" in str(path):
|
|
44
|
+
raise ValueError(f"{label} requires a local path")
|
|
45
|
+
resolved = Path(path).resolve()
|
|
46
|
+
if not resolved.is_file():
|
|
47
|
+
raise ValueError(f"{label} is missing: {resolved}")
|
|
48
|
+
return resolved
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def require_declared_path(value, label):
|
|
52
|
+
"""Resolve a caller ``{"path": ...}`` declaration to an existing local file."""
|
|
53
|
+
if not isinstance(value, dict):
|
|
54
|
+
raise ValueError(f"{label} must be an object with a path")
|
|
55
|
+
return require_local_path(value.get("path"), label)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_json_bytes(path, label):
|
|
59
|
+
"""Return (resolved_path, raw_bytes, parsed) for one local JSON document."""
|
|
60
|
+
resolved = require_local_path(path, label)
|
|
61
|
+
raw = resolved.read_bytes()
|
|
62
|
+
try:
|
|
63
|
+
return resolved, raw, json.loads(raw)
|
|
64
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
65
|
+
raise ValueError(f"{label} is not valid JSON") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def write_json_atomic(path, value):
|
|
69
|
+
path = Path(path)
|
|
70
|
+
temporary = path.with_suffix(".writing.json")
|
|
71
|
+
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
72
|
+
temporary.replace(path)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def canonical_fraction(value, label):
|
|
76
|
+
if not isinstance(value, str):
|
|
77
|
+
raise ValueError(f"{label} must be a canonical rational string")
|
|
78
|
+
try:
|
|
79
|
+
result = Fraction(value)
|
|
80
|
+
except (ValueError, ZeroDivisionError) as exc:
|
|
81
|
+
raise ValueError(f"invalid {label}") from exc
|
|
82
|
+
if result <= 0 or value != f"{result.numerator}/{result.denominator}":
|
|
83
|
+
raise ValueError(f"{label} must be a positive canonical N/D rational")
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def probe_json(path, *ffprobe_args):
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
["ffprobe", "-v", "error", *ffprobe_args, "-of", "json", str(path)],
|
|
90
|
+
capture_output=True, text=True, timeout=600,
|
|
91
|
+
)
|
|
92
|
+
if result.returncode or result.stderr.strip():
|
|
93
|
+
raise ValueError(f"media probe failed for {path}: {result.stderr.strip()}")
|
|
94
|
+
try:
|
|
95
|
+
return json.loads(result.stdout)
|
|
96
|
+
except json.JSONDecodeError as exc:
|
|
97
|
+
raise ValueError(f"invalid media probe JSON for {path}") from exc
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_logged(command, directory, label, *, prefix="", timeout=3600):
|
|
101
|
+
"""Run one FFmpeg command, keeping ``<prefix><label>.command.json`` and ``.log``."""
|
|
102
|
+
directory = Path(directory)
|
|
103
|
+
name = f"{prefix}{label}"
|
|
104
|
+
write_json_atomic(directory / f"{name}.command.json", command)
|
|
105
|
+
result = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
|
106
|
+
(directory / f"{name}.log").write_text(result.stderr, encoding="utf-8")
|
|
107
|
+
if result.returncode:
|
|
108
|
+
raise RuntimeError(f"{name} FFmpeg failed; see {name}.log")
|
|
109
|
+
return result
|