@oh-story/dsh 0.1.9 → 0.1.11
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 +14 -14
- package/lib/client.js +209 -73
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +248 -168
- package/lib/drama/skills/short-drama/SKILL.md +16 -4
- 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/creator_markdown_check.py +14 -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/character-and-look.md +10 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +6 -9
- 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-assets/references/voice-direction.md +28 -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 +65 -44
- 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 +6 -4
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +26 -67
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +31 -56
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +42 -78
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +44 -92
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +14 -16
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1272 -125
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +113 -9
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- 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-produce/SKILL.md +5 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +14 -20
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +8 -26
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +142 -1
- package/lib/drama/skills/short-drama-review/SKILL.md +4 -5
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +6 -19
- 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 +24 -35
- package/lib/drama/skills/short-drama-review/references/stage-contract.md +3 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +23 -11
- package/lib/drama/skills/short-drama-storyboard/references/blocking-playbooks.md +1 -1
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- 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 +139 -34
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +5 -3
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +15 -12
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +29 -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 +52 -39
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +24 -21
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +9 -23
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +14 -26
- package/lib/drama/skills/short-drama-video-prompts/references/review-and-fixtures.md +1 -1
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +2 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +34 -9
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -2
- 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 +10 -7
- 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 +346 -98
- package/lib/index.js.map +4 -4
- package/lib/novel-to-game/README.md +299 -59
- package/lib/novel-to-game/README_ZH.md +299 -56
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +3 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +11 -3
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +397 -8
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +0 -31
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +71 -73
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +78 -89
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +37 -14
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +5 -5
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -1
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +11 -16
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +11 -11
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +23 -0
- package/lib/novel-to-game/manifest.json +77 -87
- package/lib/novel-to-game/skills/game-art-direction/SKILL.md +7 -12
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +27 -106
- package/lib/novel-to-game/skills/game-build/SKILL.md +13 -14
- package/lib/novel-to-game/skills/game-build/references/blender-asset-workflow.md +24 -0
- package/lib/novel-to-game/skills/game-build/references/build-brief-contract.md +45 -82
- package/lib/novel-to-game/skills/game-build/references/generative-media-pipeline.md +14 -45
- package/lib/novel-to-game/skills/game-build/references/playable-model-contract.md +30 -65
- package/lib/novel-to-game/skills/game-build/references/tts-production-contract.md +13 -27
- package/lib/novel-to-game/skills/game-concept/SKILL.md +13 -15
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +6 -7
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +9 -5
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +12 -16
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +23 -72
- package/lib/novel-to-game/skills/game-world-design/references/numeric-design-method.md +14 -48
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +36 -103
- package/lib/novel-to-game/skills/novel-game-analyze/SKILL.md +4 -10
- package/lib/novel-to-game/skills/novel-game-analyze/references/gameability-protocol.md +4 -4
- package/lib/novel-to-game/skills/novel-to-game/SKILL.md +9 -21
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -4
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +5 -8
- package/lib/oh-story/manifest.json +607 -327
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/character-designer.md +1 -2
- package/lib/oh-story/roles/consistency-checker.md +15 -13
- package/lib/oh-story/roles/narrative-writer.md +36 -38
- 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 +15 -9
- 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 +46 -94
- package/lib/oh-story/skills/story/scripts/author_memory_commit.py +934 -170
- package/lib/oh-story/skills/story-cover/SKILL.md +10 -1
- package/lib/oh-story/skills/story-deslop/SKILL.md +59 -73
- 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 +12 -11
- 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 +46 -94
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +2 -2
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +22 -5
- 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 +934 -170
- package/lib/oh-story/skills/story-deslop/scripts/check-ai-patterns.js +155 -8
- package/lib/oh-story/skills/story-import/SKILL.md +64 -584
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- 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/state-tracking.md +4 -69
- package/lib/oh-story/skills/story-import/references/structure-mapping-long.md +98 -92
- 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 +15 -11
- 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 +132 -27
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +478 -15
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +182 -23
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +81 -270
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +252 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +3 -3
- 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/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +138 -64
- package/lib/oh-story/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
- 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 +55 -128
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +6 -6
- package/lib/oh-story/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
- package/lib/oh-story/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
- package/lib/oh-story/skills/story-long-scan/SKILL.md +45 -194
- 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 +49 -81
- 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 +12 -11
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +24 -202
- 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 +46 -94
- 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 +7 -11
- package/lib/oh-story/skills/story-long-write/references/character-relations.md +49 -1
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/deslop-gates.md +171 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +4 -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/emotional-methods.md +2 -2
- 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 +7 -15
- 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 +19 -13
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +13 -5
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +5 -6
- 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 +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +81 -121
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +41 -139
- 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 +57 -67
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +112 -253
- 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 +6 -4
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +19 -65
- package/lib/oh-story/skills/story-long-write/scripts/author_memory_commit.py +934 -170
- 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 +226 -25
- 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 +132 -27
- 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 +7 -4
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +304 -46
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +478 -15
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +182 -23
- package/lib/oh-story/skills/story-review/SKILL.md +31 -302
- 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 +12 -11
- 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 +46 -94
- 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/dialogue-mastery.md +4 -0
- package/lib/oh-story/skills/story-review/references/review-quality.md +6 -11
- 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 +15 -11
- package/lib/oh-story/skills/story-review/scripts/author_memory_commit.py +934 -170
- 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 +478 -15
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +182 -23
- package/lib/oh-story/skills/story-setup/SKILL.md +71 -452
- 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 +12 -11
- 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 +22 -5
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +4 -0
- 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-emotional-methods.md +2 -2
- 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 +19 -65
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +4 -4
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +56 -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/stage4-6-reversal-summary.md +611 -0
- package/lib/oh-story/skills/story-short-scan/SKILL.md +35 -65
- 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 -175
- 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 +46 -94
- 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/dialogue-mastery.md +4 -0
- 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 -47
- 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 +21 -13
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +116 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +99 -0
- 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 +934 -170
- 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/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +19 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -0
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +16 -0
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/client/video-studio.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts +5 -3
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/native-hooks.d.ts +53 -1
- package/lib/types/native-hooks.d.ts.map +1 -1
- package/lib/types/role-identity.d.ts +17 -0
- package/lib/types/role-identity.d.ts.map +1 -0
- 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-script/scripts/agent_brief.py → video-understanding/scripts/briefing/builder.py} +38 -35
- package/lib/video-recap/skills/video-understanding/scripts/{brief_context.py → 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-script/scripts/brief_timeline.py → video-understanding/scripts/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 +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -58
- package/lib/novel-to-game/skills/game-world-design/references/game-writing-craft.md +0 -53
- package/lib/novel-to-game/skills/novel-to-game/references/intake-benchmark-reference.md +0 -38
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +0 -530
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +0 -559
- package/lib/oh-story/skills/story-long-scan/references/scan-output-format.md +0 -207
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +0 -678
- 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/brief_context.py +0 -348
- package/lib/video-recap/skills/video-script/scripts/narration.py +0 -12
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +0 -399
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +0 -290
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +0 -327
- /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,469 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Prepare exact source-audio and continuous-score beds from a strict sample plan."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import math
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
|
|
14
|
+
from assemble_constants import SUPPORTED_PICTURE_CODECS, frame_clock_samples
|
|
15
|
+
from adoption.strict_inputs import (
|
|
16
|
+
canonical_fraction, probe_json, read_json_bytes, require_declared_path, require_fields,
|
|
17
|
+
require_integer, require_local_path, require_number, run_logged, without_digests,
|
|
18
|
+
write_json_atomic,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
RATE = 48_000
|
|
23
|
+
CHANNELS = 2
|
|
24
|
+
CODEC = "pcm_f32le"
|
|
25
|
+
SOURCE_ROLES = {"protected_original", "mixed_original_under_narration"}
|
|
26
|
+
FADE_SHAPES = {"linear", "half_cosine"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _probe_cfr(path):
|
|
30
|
+
data = probe_json(
|
|
31
|
+
path, "-select_streams", "v:0", "-show_streams", "-show_packets",
|
|
32
|
+
"-show_entries",
|
|
33
|
+
"stream=codec_name,time_base,start_pts,start_time,duration_ts,duration,"
|
|
34
|
+
"avg_frame_rate,r_frame_rate:packet=pts,duration",
|
|
35
|
+
)
|
|
36
|
+
streams = data.get("streams", [])
|
|
37
|
+
if len(streams) != 1:
|
|
38
|
+
raise ValueError("source requires exactly one selected v:0 clock")
|
|
39
|
+
stream = streams[0]
|
|
40
|
+
codec = stream.get("codec_name")
|
|
41
|
+
if codec not in SUPPORTED_PICTURE_CODECS:
|
|
42
|
+
raise ValueError("v1 packet/frame CFR proof supports H264 and HEVC picture sources only")
|
|
43
|
+
average = canonical_fraction(stream.get("avg_frame_rate"), "actual average frame rate")
|
|
44
|
+
real = canonical_fraction(stream.get("r_frame_rate"), "actual real frame rate")
|
|
45
|
+
if average != real:
|
|
46
|
+
raise ValueError("source must be same-speed CFR")
|
|
47
|
+
start_time = Fraction(stream.get("start_time", "0"))
|
|
48
|
+
if start_time != 0:
|
|
49
|
+
raise ValueError("source video frame clock must start at zero")
|
|
50
|
+
time_base = Fraction(stream["time_base"])
|
|
51
|
+
packets = data.get("packets", [])
|
|
52
|
+
try:
|
|
53
|
+
pts = sorted(Fraction(int(packet["pts"])) * time_base for packet in packets)
|
|
54
|
+
durations = [Fraction(int(packet["duration"])) * time_base for packet in packets]
|
|
55
|
+
except (KeyError, TypeError, ValueError, ZeroDivisionError) as exc:
|
|
56
|
+
raise ValueError("H26x packet/frame clock proof is incomplete") from exc
|
|
57
|
+
frame_duration = 1 / average
|
|
58
|
+
count = len(pts)
|
|
59
|
+
if not pts or any(value != index * frame_duration for index, value in enumerate(pts)):
|
|
60
|
+
raise ValueError("source picture packets do not prove a zero-origin CFR frame clock")
|
|
61
|
+
if any(duration != frame_duration for duration in durations):
|
|
62
|
+
raise ValueError("source picture packet durations are not one CFR frame")
|
|
63
|
+
return {"fps": str(average), "frame_count": count,
|
|
64
|
+
"time_base": stream.get("time_base"), "start_time": "0",
|
|
65
|
+
"codec_name": codec, "clock_proof": "H26X_ONE_PACKET_PER_FRAME_PTS"}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _probe_audio(path, ordinal):
|
|
69
|
+
data = probe_json(
|
|
70
|
+
path, "-select_streams", f"a:{ordinal}", "-show_streams",
|
|
71
|
+
"-show_entries", "stream=index,codec_name,sample_fmt,sample_rate,channels,"
|
|
72
|
+
"channel_layout,time_base,start_pts,start_time,duration_ts,duration",
|
|
73
|
+
)
|
|
74
|
+
streams = data.get("streams", [])
|
|
75
|
+
if len(streams) != 1:
|
|
76
|
+
raise ValueError(f"audio stream a:{ordinal} is missing or ambiguous")
|
|
77
|
+
stream = streams[0]
|
|
78
|
+
return {key: stream.get(key) for key in (
|
|
79
|
+
"index", "codec_name", "sample_fmt", "sample_rate", "channels",
|
|
80
|
+
"channel_layout", "time_base", "start_pts", "start_time", "duration_ts", "duration"
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _probe_pcm(path):
|
|
85
|
+
stream = _probe_audio(path, 0)
|
|
86
|
+
if stream["codec_name"] not in {"pcm_s16le", "pcm_s24le", "pcm_f32le"}:
|
|
87
|
+
raise ValueError("canonical/frozen WAV requires PCM16, PCM24, or PCM float")
|
|
88
|
+
if stream["sample_rate"] != str(RATE) or stream["channels"] != CHANNELS:
|
|
89
|
+
raise ValueError("canonical/frozen WAV must be 48 kHz stereo")
|
|
90
|
+
time_base = Fraction(stream["time_base"])
|
|
91
|
+
samples = int(Fraction(stream["duration_ts"]) * time_base * RATE)
|
|
92
|
+
if Fraction(stream["duration_ts"]) * time_base * RATE != samples:
|
|
93
|
+
raise ValueError("WAV duration is not an integral 48 kHz sample count")
|
|
94
|
+
return {"codec_name": stream["codec_name"], "sample_fmt": stream["sample_fmt"],
|
|
95
|
+
"sample_rate": RATE, "channels": CHANNELS, "samples": samples}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_fades(value, duration, curves, label):
|
|
99
|
+
fade_in = require_integer(value["fade_in_samples"], f"{label} fade_in_samples")
|
|
100
|
+
fade_out = require_integer(value["fade_out_samples"], f"{label} fade_out_samples")
|
|
101
|
+
if fade_in + fade_out > duration:
|
|
102
|
+
raise ValueError(f"{label} fades overlap")
|
|
103
|
+
if fade_in == 1 or fade_out == 1:
|
|
104
|
+
raise ValueError(f"{label} fades require zero or at least two samples")
|
|
105
|
+
if value["fade_shape"] not in curves:
|
|
106
|
+
raise ValueError(f"unsupported {label} fade_shape")
|
|
107
|
+
return fade_in, fade_out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load_plan(plan_path):
|
|
111
|
+
plan_path, _, plan = read_json_bytes(plan_path, "source score plan")
|
|
112
|
+
require_fields(plan, ["artifact", "schema_version", "output", "source_segments",
|
|
113
|
+
"source_silence", "score"], "source score plan")
|
|
114
|
+
if plan["artifact"] != "source_score_plan" or type(plan["schema_version"]) is not int \
|
|
115
|
+
or plan["schema_version"] != 1:
|
|
116
|
+
raise ValueError("unsupported source_score_plan schema")
|
|
117
|
+
require_fields(plan["output"], ["sample_rate", "channels", "total_samples"], "output")
|
|
118
|
+
if plan["output"]["sample_rate"] != RATE or plan["output"]["channels"] != CHANNELS:
|
|
119
|
+
raise ValueError("v1 output is fixed at 48 kHz stereo")
|
|
120
|
+
total = require_integer(plan["output"]["total_samples"], "total_samples", 1)
|
|
121
|
+
if not isinstance(plan["source_segments"], list) or not isinstance(plan["source_silence"], list):
|
|
122
|
+
raise ValueError("source segments and explicit silence must be lists")
|
|
123
|
+
segments = []
|
|
124
|
+
asset_cache = {}
|
|
125
|
+
picture_cache = {}
|
|
126
|
+
ids = set()
|
|
127
|
+
segment_fields = [
|
|
128
|
+
"id", "path", "audio_stream", "source_fps", "source_start_frame",
|
|
129
|
+
"source_end_frame", "output_start_sample", "gain", "fade_in_samples",
|
|
130
|
+
"fade_out_samples", "fade_shape", "role",
|
|
131
|
+
]
|
|
132
|
+
for value in plan["source_segments"]:
|
|
133
|
+
value = without_digests(value, "source segment")
|
|
134
|
+
require_fields(value, segment_fields, "source segment")
|
|
135
|
+
if not isinstance(value["id"], str) or not value["id"] or value["id"] in ids:
|
|
136
|
+
raise ValueError("source segment id must be unique and non-empty")
|
|
137
|
+
ids.add(value["id"])
|
|
138
|
+
ordinal = require_integer(value["audio_stream"], "audio_stream")
|
|
139
|
+
path = require_declared_path(value, "source")
|
|
140
|
+
fps = canonical_fraction(value["source_fps"], "source_fps")
|
|
141
|
+
start = require_integer(value["source_start_frame"], "source_start_frame")
|
|
142
|
+
end = require_integer(value["source_end_frame"], "source_end_frame", 1)
|
|
143
|
+
if end <= start:
|
|
144
|
+
raise ValueError("source frame interval must be non-empty")
|
|
145
|
+
key = (str(path), ordinal)
|
|
146
|
+
if key not in asset_cache:
|
|
147
|
+
if str(path) not in picture_cache:
|
|
148
|
+
picture_cache[str(path)] = _probe_cfr(path)
|
|
149
|
+
picture = picture_cache[str(path)]
|
|
150
|
+
audio = _probe_audio(path, ordinal)
|
|
151
|
+
asset_cache[key] = {"path": str(path), "audio_stream": ordinal,
|
|
152
|
+
"picture": picture, "audio": audio}
|
|
153
|
+
facts = asset_cache[key]
|
|
154
|
+
if facts["picture"]["fps"] != str(fps) or end > facts["picture"]["frame_count"]:
|
|
155
|
+
raise ValueError("declared source frame clock/range differs from actual CFR source")
|
|
156
|
+
source_start_sample = frame_clock_samples(start, fps, RATE)
|
|
157
|
+
source_end_sample = frame_clock_samples(end, fps, RATE)
|
|
158
|
+
duration = source_end_sample - source_start_sample
|
|
159
|
+
if duration <= 0:
|
|
160
|
+
raise ValueError("source frame interval is shorter than one 48 kHz sample")
|
|
161
|
+
fade_in, fade_out = _validate_fades(value, duration, {"linear"}, "source")
|
|
162
|
+
output_start = require_integer(value["output_start_sample"], "output_start_sample")
|
|
163
|
+
output_end = output_start + duration
|
|
164
|
+
if output_end > total or value["role"] not in SOURCE_ROLES:
|
|
165
|
+
raise ValueError("source output range or role is unsupported")
|
|
166
|
+
segments.append({**value, "path": str(path),
|
|
167
|
+
"gain": require_number(value["gain"], "source gain", 0, 16),
|
|
168
|
+
"source_start_sample": source_start_sample,
|
|
169
|
+
"source_end_sample": source_end_sample,
|
|
170
|
+
"output_end_sample": output_end, "fade_in_samples": fade_in,
|
|
171
|
+
"fade_out_samples": fade_out, "asset_key": key})
|
|
172
|
+
silence = []
|
|
173
|
+
for value in plan["source_silence"]:
|
|
174
|
+
require_fields(value, ["output_start_sample", "output_end_sample", "role"], "source silence")
|
|
175
|
+
start = require_integer(value["output_start_sample"], "silence output_start_sample")
|
|
176
|
+
end = require_integer(value["output_end_sample"], "silence output_end_sample", 1)
|
|
177
|
+
if value["role"] != "silence" or not start < end <= total:
|
|
178
|
+
raise ValueError("invalid explicit source silence range")
|
|
179
|
+
silence.append(dict(value))
|
|
180
|
+
coverage = sorted(
|
|
181
|
+
[(item["output_start_sample"], item["output_end_sample"]) for item in segments]
|
|
182
|
+
+ [(item["output_start_sample"], item["output_end_sample"]) for item in silence]
|
|
183
|
+
)
|
|
184
|
+
cursor = 0
|
|
185
|
+
for start, end in coverage:
|
|
186
|
+
if start != cursor:
|
|
187
|
+
raise ValueError("source bed requires exact nonoverlapping coverage with explicit silence")
|
|
188
|
+
cursor = end
|
|
189
|
+
if cursor != total:
|
|
190
|
+
raise ValueError("source bed has an implicit tail gap")
|
|
191
|
+
score = plan["score"]
|
|
192
|
+
if not isinstance(score, dict) or score.get("kind") not in {"raw", "frozen", "none"}:
|
|
193
|
+
raise ValueError("score kind must be raw, frozen, or none")
|
|
194
|
+
score = without_digests(score, "score")
|
|
195
|
+
if score["kind"] == "raw":
|
|
196
|
+
require_fields(score, ["kind", "path", "audio_stream", "source_offset_sample",
|
|
197
|
+
"gain", "fade_in_samples", "fade_out_samples", "fade_shape"], "raw score")
|
|
198
|
+
score_path = require_declared_path(score, "raw score")
|
|
199
|
+
ordinal = require_integer(score["audio_stream"], "score audio_stream")
|
|
200
|
+
offset = require_integer(score["source_offset_sample"], "score source_offset_sample")
|
|
201
|
+
fade_in, fade_out = _validate_fades(score, total, FADE_SHAPES, "score")
|
|
202
|
+
score = {**score, "path": str(score_path),
|
|
203
|
+
"audio_stream": ordinal, "source_offset_sample": offset,
|
|
204
|
+
"gain": require_number(score["gain"], "score gain", 0, 16),
|
|
205
|
+
"fade_in_samples": fade_in, "fade_out_samples": fade_out,
|
|
206
|
+
"audio": _probe_audio(score_path, ordinal)}
|
|
207
|
+
elif score["kind"] == "frozen":
|
|
208
|
+
require_fields(score, ["kind", "path", "audio_stream"], "frozen score")
|
|
209
|
+
score_path = require_declared_path(score, "frozen score")
|
|
210
|
+
ordinal = require_integer(score["audio_stream"], "score audio_stream")
|
|
211
|
+
if ordinal != 0:
|
|
212
|
+
raise ValueError("frozen WAV supports only a:0")
|
|
213
|
+
pcm = _probe_pcm(score_path)
|
|
214
|
+
if pcm["samples"] != total:
|
|
215
|
+
raise ValueError("frozen score must exactly match total_samples")
|
|
216
|
+
score = {**score, "path": str(score_path), "pcm": pcm}
|
|
217
|
+
else:
|
|
218
|
+
require_fields(score, ["kind"], "none score")
|
|
219
|
+
return {"plan_path": plan_path,
|
|
220
|
+
"output": {**plan["output"], "codec": CODEC}, "source_segments": segments,
|
|
221
|
+
"source_silence": silence, "source_assets": list(asset_cache.values()),
|
|
222
|
+
"score": score}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
_run = run_logged
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _decode_command(path, ordinal, output):
|
|
229
|
+
return ["ffmpeg", "-nostdin", "-v", "error", "-n", "-copyts", "-i", str(path),
|
|
230
|
+
"-map", f"0:a:{ordinal}", "-af",
|
|
231
|
+
"aresample=48000:async=0:first_pts=0,aformat=sample_fmts=flt:"
|
|
232
|
+
"sample_rates=48000:channel_layouts=stereo", "-c:a", CODEC, str(output)]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _fade_filters(duration, fade_in, fade_out, curve):
|
|
236
|
+
factors = []
|
|
237
|
+
if fade_in:
|
|
238
|
+
if curve == "linear":
|
|
239
|
+
factors.append(f"min(1\\,n/{fade_in - 1})")
|
|
240
|
+
else:
|
|
241
|
+
position = f"max(0\\,min(1\\,n/{fade_in - 1}))"
|
|
242
|
+
factors.append(f"0.5-0.5*cos(PI*{position})")
|
|
243
|
+
if fade_out:
|
|
244
|
+
remaining = f"({duration - 1}-n)"
|
|
245
|
+
if curve == "linear":
|
|
246
|
+
factors.append(f"max(0\\,min(1\\,{remaining}/{fade_out - 1}))")
|
|
247
|
+
else:
|
|
248
|
+
position = f"max(0\\,min(1\\,{remaining}/{fade_out - 1}))"
|
|
249
|
+
factors.append(
|
|
250
|
+
f"0.5-0.5*cos(PI*{position})"
|
|
251
|
+
)
|
|
252
|
+
if not factors:
|
|
253
|
+
return []
|
|
254
|
+
gain = "*".join(f"({factor})" for factor in factors)
|
|
255
|
+
return [f"aeval=val(0)*({gain})|val(1)*({gain}):c=same"]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _render_source(plan, decoded, output, directory):
|
|
259
|
+
total = plan["output"]["total_samples"]
|
|
260
|
+
command = ["ffmpeg", "-nostdin", "-v", "error", "-n"]
|
|
261
|
+
assets = {tuple(asset_key): index for index, asset_key in enumerate(decoded)}
|
|
262
|
+
for asset_key in decoded:
|
|
263
|
+
command += ["-i", str(decoded[asset_key])]
|
|
264
|
+
filters = [f"anullsrc=r={RATE}:cl=stereo,atrim=end_sample={total}[base]"]
|
|
265
|
+
inputs = ["[base]"]
|
|
266
|
+
for index, segment in enumerate(plan["source_segments"]):
|
|
267
|
+
duration = segment["source_end_sample"] - segment["source_start_sample"]
|
|
268
|
+
chain = [f"[{assets[tuple(segment['asset_key'])]}:a]atrim="
|
|
269
|
+
f"start_sample={segment['source_start_sample']}:"
|
|
270
|
+
f"end_sample={segment['source_end_sample']}", "asetpts=PTS-STARTPTS",
|
|
271
|
+
f"volume={segment['gain']:.17g}"]
|
|
272
|
+
chain += _fade_filters(duration, segment["fade_in_samples"],
|
|
273
|
+
segment["fade_out_samples"], "linear")
|
|
274
|
+
chain.append(f"adelay={segment['output_start_sample']}S:all=1[src{index}]")
|
|
275
|
+
filters.append(",".join(chain))
|
|
276
|
+
inputs.append(f"[src{index}]")
|
|
277
|
+
filters.append("".join(inputs) + f"amix=inputs={len(inputs)}:duration=first:normalize=0,"
|
|
278
|
+
f"atrim=end_sample={total},aformat=sample_fmts=flt:"
|
|
279
|
+
f"sample_rates={RATE}:channel_layouts=stereo[out]")
|
|
280
|
+
command += ["-filter_complex", ";".join(filters), "-map", "[out]", "-c:a", CODEC,
|
|
281
|
+
str(output)]
|
|
282
|
+
_run(command, directory, "source")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _render_score(plan, decoded_score, output, directory):
|
|
286
|
+
total = plan["output"]["total_samples"]
|
|
287
|
+
score = plan["score"]
|
|
288
|
+
if score["kind"] == "none":
|
|
289
|
+
command = [
|
|
290
|
+
"ffmpeg", "-nostdin", "-v", "error", "-n", "-f", "lavfi", "-i",
|
|
291
|
+
f"anullsrc=r={RATE}:cl=stereo", "-af", f"atrim=end_sample={total}",
|
|
292
|
+
"-c:a", CODEC, str(output),
|
|
293
|
+
]
|
|
294
|
+
elif score["kind"] == "frozen":
|
|
295
|
+
command = ["ffmpeg", "-nostdin", "-v", "error", "-n", "-i", score["path"],
|
|
296
|
+
"-map", "0:a:0", "-af", "aformat=sample_fmts=flt:sample_rates=48000:"
|
|
297
|
+
"channel_layouts=stereo", "-c:a", CODEC, str(output)]
|
|
298
|
+
else:
|
|
299
|
+
chain = [f"atrim=start_sample={score['source_offset_sample']}:"
|
|
300
|
+
f"end_sample={score['source_offset_sample'] + total}",
|
|
301
|
+
"asetpts=PTS-STARTPTS", f"volume={score['gain']:.17g}"]
|
|
302
|
+
chain += _fade_filters(total, score["fade_in_samples"], score["fade_out_samples"],
|
|
303
|
+
score["fade_shape"])
|
|
304
|
+
chain += [f"atrim=end_sample={total}", "aformat=sample_fmts=flt:sample_rates=48000:"
|
|
305
|
+
"channel_layouts=stereo"]
|
|
306
|
+
command = ["ffmpeg", "-nostdin", "-v", "error", "-n", "-i", str(decoded_score),
|
|
307
|
+
"-af", ",".join(chain), "-c:a", CODEC, str(output)]
|
|
308
|
+
_run(command, directory, "score")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _astats(path):
|
|
312
|
+
result = subprocess.run(
|
|
313
|
+
["ffmpeg", "-nostdin", "-v", "info", "-i", str(path), "-af",
|
|
314
|
+
"astats=metadata=0:reset=0", "-f", "null", "-"],
|
|
315
|
+
capture_output=True, text=True, timeout=3600,
|
|
316
|
+
)
|
|
317
|
+
if result.returncode:
|
|
318
|
+
raise ValueError("output astats decode failed")
|
|
319
|
+
peaks = re.findall(r"Peak level dB:\s*(-?inf|[-+0-9.]+)", result.stderr, re.I)
|
|
320
|
+
nan_counts = re.findall(r"Number of NaNs:\s*(\d+)", result.stderr)
|
|
321
|
+
inf_counts = re.findall(r"Number of Infs:\s*(\d+)", result.stderr)
|
|
322
|
+
if not peaks:
|
|
323
|
+
raise ValueError("output peak statistics unavailable")
|
|
324
|
+
peak_db = max(float(value) if value.lower() != "-inf" else -math.inf for value in peaks)
|
|
325
|
+
finite = all(int(value) == 0 for value in nan_counts + inf_counts)
|
|
326
|
+
if not finite:
|
|
327
|
+
raise ValueError("output contains non-finite PCM samples")
|
|
328
|
+
return {"finite": True, "peak": 0.0 if peak_db == -math.inf else 10 ** (peak_db / 20)}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _output_facts(path):
|
|
332
|
+
"""PCM format, sample count, size and peak/finiteness of one 48 kHz stereo float WAV."""
|
|
333
|
+
return {"path": str(path), "bytes": os.stat(path).st_size, "pcm": _probe_pcm(path),
|
|
334
|
+
**_astats(path)}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def validate_prepared_receipt(reference, expected_format):
|
|
338
|
+
"""Validate one completed prepared-bed receipt and its three current PCM stems."""
|
|
339
|
+
require_fields(without_digests(reference, "prepared receipt reference"), ["path"],
|
|
340
|
+
"prepared receipt reference")
|
|
341
|
+
require_fields(expected_format, ["sample_rate", "channels", "total_samples"],
|
|
342
|
+
"expected prepared format")
|
|
343
|
+
if expected_format["sample_rate"] != RATE or expected_format["channels"] != CHANNELS:
|
|
344
|
+
raise ValueError("prepared format must be 48 kHz stereo")
|
|
345
|
+
require_integer(expected_format["total_samples"], "prepared total_samples", 1)
|
|
346
|
+
receipt_path = require_declared_path(reference, "prepared receipt")
|
|
347
|
+
receipt = read_json_bytes(receipt_path, "prepared receipt")[2]
|
|
348
|
+
if not isinstance(receipt, dict) or receipt.get("artifact") != "prepared_bed_receipt" \
|
|
349
|
+
or receipt.get("schema_version") != 1 or receipt.get("status") != "PREPARED":
|
|
350
|
+
raise ValueError("prepared receipt is not a completed v1 artifact")
|
|
351
|
+
required_format = {**expected_format, "codec": CODEC}
|
|
352
|
+
if receipt.get("format") != required_format:
|
|
353
|
+
raise ValueError("prepared receipt format differs from expected picture clock")
|
|
354
|
+
outputs = receipt.get("outputs")
|
|
355
|
+
if not isinstance(outputs, dict) or set(outputs) != {
|
|
356
|
+
"source_bed.wav", "score_bed.wav", "prepared_bed.wav"
|
|
357
|
+
}:
|
|
358
|
+
raise ValueError("prepared receipt requires exactly three named bed outputs")
|
|
359
|
+
prepared = {}
|
|
360
|
+
for name, declared in outputs.items():
|
|
361
|
+
if not isinstance(declared, dict):
|
|
362
|
+
raise ValueError(f"prepared receipt {name} identity is invalid")
|
|
363
|
+
path = require_local_path(declared.get("path"), name)
|
|
364
|
+
stream = _probe_audio(path, 0)
|
|
365
|
+
if stream["codec_name"] != CODEC or stream["sample_rate"] != str(RATE) or \
|
|
366
|
+
stream["channels"] != CHANNELS or \
|
|
367
|
+
(stream["start_time"] not in (None, "N/A") and
|
|
368
|
+
Fraction(stream["start_time"]) != 0):
|
|
369
|
+
raise ValueError(f"{name} must be zero-origin 48 kHz stereo float PCM")
|
|
370
|
+
actual = _output_facts(path)
|
|
371
|
+
if actual["pcm"]["codec_name"] != CODEC \
|
|
372
|
+
or actual["pcm"]["samples"] != expected_format["total_samples"]:
|
|
373
|
+
raise ValueError(f"{name} does not match the required full PCM format")
|
|
374
|
+
prepared[name] = actual
|
|
375
|
+
return {
|
|
376
|
+
"reference": {"path": str(receipt_path)},
|
|
377
|
+
"format": dict(expected_format), "outputs": prepared, "receipt": receipt,
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def prepare_source_score(plan_path, output_dir):
|
|
382
|
+
directory = Path(output_dir).resolve()
|
|
383
|
+
directory.mkdir(parents=True, exist_ok=False)
|
|
384
|
+
finals = {name: directory / name for name in
|
|
385
|
+
("source_bed.wav", "score_bed.wav", "prepared_bed.wav")}
|
|
386
|
+
staged = {name: directory / f".{Path(name).stem}.rendering.wav" for name in finals}
|
|
387
|
+
receipt_path = directory / "prepared_bed_receipt.json"
|
|
388
|
+
try:
|
|
389
|
+
plan = load_plan(plan_path)
|
|
390
|
+
decoded = {}
|
|
391
|
+
for index, asset in enumerate(plan["source_assets"]):
|
|
392
|
+
key = (asset["path"], asset["audio_stream"])
|
|
393
|
+
path = directory / f".source_{index:03d}.decoded.wav"
|
|
394
|
+
_run(_decode_command(asset["path"], asset["audio_stream"], path),
|
|
395
|
+
directory, f"decode_source_{index:03d}")
|
|
396
|
+
decoded[key] = path
|
|
397
|
+
asset["canonical_pcm"] = _output_facts(path)
|
|
398
|
+
required = max(
|
|
399
|
+
segment["source_end_sample"] for segment in plan["source_segments"]
|
|
400
|
+
if tuple(segment["asset_key"]) == key
|
|
401
|
+
)
|
|
402
|
+
if asset["canonical_pcm"]["pcm"]["samples"] < required:
|
|
403
|
+
raise ValueError("decoded source audio is too short for a selected frame range")
|
|
404
|
+
score_decode = None
|
|
405
|
+
if plan["score"]["kind"] == "raw":
|
|
406
|
+
score_decode = directory / ".score.decoded.wav"
|
|
407
|
+
_run(_decode_command(plan["score"]["path"], plan["score"]["audio_stream"],
|
|
408
|
+
score_decode), directory, "decode_score")
|
|
409
|
+
plan["score"]["canonical_decode"] = _output_facts(score_decode)
|
|
410
|
+
if plan["score"]["canonical_decode"]["pcm"]["samples"] < \
|
|
411
|
+
plan["score"]["source_offset_sample"] + plan["output"]["total_samples"]:
|
|
412
|
+
raise ValueError("raw score is too short for continuous offset window")
|
|
413
|
+
_render_source(plan, decoded, staged["source_bed.wav"], directory)
|
|
414
|
+
_render_score(plan, score_decode, staged["score_bed.wav"], directory)
|
|
415
|
+
rendered = {
|
|
416
|
+
"source_bed.wav": _output_facts(staged["source_bed.wav"]),
|
|
417
|
+
"score_bed.wav": _output_facts(staged["score_bed.wav"]),
|
|
418
|
+
}
|
|
419
|
+
if plan["score"]["kind"] == "none":
|
|
420
|
+
shutil.copyfile(staged["source_bed.wav"], staged["prepared_bed.wav"])
|
|
421
|
+
else:
|
|
422
|
+
command = [
|
|
423
|
+
"ffmpeg", "-nostdin", "-v", "error", "-n",
|
|
424
|
+
"-i", str(staged["source_bed.wav"]), "-i", str(staged["score_bed.wav"]),
|
|
425
|
+
"-filter_complex", f"[0:a][1:a]amix=inputs=2:duration=first:normalize=0,"
|
|
426
|
+
f"atrim=end_sample={plan['output']['total_samples']},aformat=sample_fmts=flt:"
|
|
427
|
+
"sample_rates=48000:channel_layouts=stereo[out]", "-map", "[out]",
|
|
428
|
+
"-c:a", CODEC, str(staged["prepared_bed.wav"]),
|
|
429
|
+
]
|
|
430
|
+
_run(command, directory, "prepare")
|
|
431
|
+
outputs = {**rendered, "prepared_bed.wav": _output_facts(staged["prepared_bed.wav"])}
|
|
432
|
+
if any(value["pcm"]["samples"] != plan["output"]["total_samples"]
|
|
433
|
+
for value in outputs.values()):
|
|
434
|
+
raise ValueError("prepared bed sample counts differ from plan")
|
|
435
|
+
if plan["score"]["kind"] == "none" and (
|
|
436
|
+
outputs["prepared_bed.wav"]["bytes"] != outputs["source_bed.wav"]["bytes"]
|
|
437
|
+
or outputs["prepared_bed.wav"]["pcm"] != outputs["source_bed.wav"]["pcm"]):
|
|
438
|
+
raise ValueError("none score must preserve the source bed exactly")
|
|
439
|
+
outputs["prepared_bed.wav"]["headroom_policy"] = "FLOAT_PRESERVED_NO_MASTER"
|
|
440
|
+
receipt = {
|
|
441
|
+
"artifact": "prepared_bed_receipt", "schema_version": 1, "status": "PREPARED",
|
|
442
|
+
"plan": {"path": str(plan["plan_path"])},
|
|
443
|
+
"format": plan["output"], "source_assets": plan["source_assets"],
|
|
444
|
+
"source_segments": [{key: value for key, value in item.items() if key != "asset_key"}
|
|
445
|
+
for item in plan["source_segments"]],
|
|
446
|
+
"source_silence": plan["source_silence"], "score": plan["score"],
|
|
447
|
+
"outputs": outputs, "direct_listening": "NOT_CHECKED", "release_approved": False,
|
|
448
|
+
}
|
|
449
|
+
for name, path in staged.items():
|
|
450
|
+
path.rename(finals[name])
|
|
451
|
+
receipt["outputs"][name]["path"] = str(finals[name])
|
|
452
|
+
write_json_atomic(receipt_path, receipt)
|
|
453
|
+
return receipt
|
|
454
|
+
except Exception:
|
|
455
|
+
for path in [*staged.values(), *finals.values(), receipt_path]:
|
|
456
|
+
path.unlink(missing_ok=True)
|
|
457
|
+
raise
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def main():
|
|
461
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
462
|
+
parser.add_argument("plan")
|
|
463
|
+
parser.add_argument("--output-dir", required=True)
|
|
464
|
+
args = parser.parse_args()
|
|
465
|
+
print(json.dumps(prepare_source_score(args.plan, args.output_dir), ensure_ascii=False))
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
if __name__ == "__main__":
|
|
469
|
+
main()
|
|
@@ -10,13 +10,15 @@ from lib import CONFIG
|
|
|
10
10
|
from media import _plan_clip_spans
|
|
11
11
|
from assemble_constants import (
|
|
12
12
|
_AUTO_ORIGINAL_READ_CPS,
|
|
13
|
+
_CLIP_CONTIGUITY_TOLERANCE,
|
|
13
14
|
_MAX_ORIGINAL_READ_CPS,
|
|
14
15
|
_MIN_ASR_CLIP_OVERLAP,
|
|
15
16
|
_MIN_GAP_TO_SUBTITLE,
|
|
16
17
|
_MIN_READABLE_SECONDS,
|
|
17
18
|
_SUBTITLE_CLOSING_QUOTES,
|
|
18
19
|
)
|
|
19
|
-
from
|
|
20
|
+
from subtitles.track_binding import bound_subtitle_entries
|
|
21
|
+
from subtitles.core import (
|
|
20
22
|
_bracketed_original_chunks,
|
|
21
23
|
_subtitle_entries,
|
|
22
24
|
)
|
|
@@ -214,21 +216,32 @@ def _load_user_original_subtitles(work_dir):
|
|
|
214
216
|
|
|
215
217
|
def _map_asr_to_output(asr_segs, clip_spans):
|
|
216
218
|
"""Map SOURCE-time ASR segments onto the OUTPUT timeline. Full mode (clip_spans None) is
|
|
217
|
-
identity
|
|
218
|
-
|
|
219
|
+
identity. Keep one utterance across source/output-continuous picture cuts;
|
|
220
|
+
real source deletions, output gaps and repeated playback remain separate."""
|
|
219
221
|
if clip_spans is None:
|
|
220
222
|
return [dict(s) for s in asr_segs]
|
|
221
223
|
out = []
|
|
222
224
|
for seg in asr_segs:
|
|
225
|
+
fragments = []
|
|
223
226
|
for c in clip_spans:
|
|
224
227
|
ov_s, ov_e = max(seg["start"], c["source_start"]), min(seg["end"], c["source_end"])
|
|
225
|
-
if ov_e
|
|
228
|
+
if ov_e <= ov_s:
|
|
226
229
|
continue
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
start = c["output_start"] + (ov_s - c["source_start"])
|
|
231
|
+
end = c["output_start"] + (ov_e - c["source_start"])
|
|
232
|
+
entry = c.get("entry", {})
|
|
233
|
+
source_key = (c.get("source_id") or entry.get("source_id"),
|
|
234
|
+
c.get("source_path") or entry.get("source_path"))
|
|
235
|
+
if (fragments and abs(fragments[-1]["source_end"] - ov_s) < _CLIP_CONTIGUITY_TOLERANCE
|
|
236
|
+
and abs(fragments[-1]["end"] - start) < _CLIP_CONTIGUITY_TOLERANCE
|
|
237
|
+
and fragments[-1]["source_key"] == source_key):
|
|
238
|
+
fragments[-1].update(end=end, source_end=ov_e)
|
|
239
|
+
else:
|
|
240
|
+
fragments.append({"start": start, "end": end, "source_end": ov_e,
|
|
241
|
+
"source_key": source_key})
|
|
242
|
+
# Filter after joining: individually tiny pieces may be one readable phrase.
|
|
243
|
+
out.extend({"start": f["start"], "end": f["end"], "text": seg["text"]}
|
|
244
|
+
for f in fragments if f["end"] - f["start"] > _MIN_ASR_CLIP_OVERLAP)
|
|
232
245
|
return out
|
|
233
246
|
|
|
234
247
|
|
|
@@ -394,6 +407,9 @@ def _sentence_subspan(seg, sentence):
|
|
|
394
407
|
def _combined_subtitle_entries(narration, work_dir, video_duration):
|
|
395
408
|
"""Narration subtitle entries plus original-dialogue entries in the gaps, sorted by start.
|
|
396
409
|
Original entries are confined to narration gaps, so they never overlap narration entries."""
|
|
410
|
+
bound = bound_subtitle_entries(work_dir, video_duration)
|
|
411
|
+
if bound is not None:
|
|
412
|
+
return bound
|
|
397
413
|
entries = _subtitle_entries(narration)
|
|
398
414
|
entries.extend(_original_gap_subtitle_entries(narration, work_dir, video_duration))
|
|
399
415
|
entries.sort(key=lambda x: (x["start"], x["end"]))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Subtitle generation and track-binding subpackage."""
|
package/lib/video-recap/skills/video-assemble/scripts/{subtitle_core.py → subtitles/core.py}
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
+
from decimal import Decimal
|
|
5
6
|
|
|
6
7
|
from lib import CONFIG
|
|
7
8
|
from assemble_constants import (
|
|
@@ -13,11 +14,16 @@ from assemble_constants import (
|
|
|
13
14
|
from media import _ratio_to_float
|
|
14
15
|
|
|
15
16
|
def _seconds_to_srt_time(seconds):
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
"""Floor times to SRT milliseconds without float remainder artifacts.
|
|
18
|
+
|
|
19
|
+
Coercing first keeps Fraction/Decimal/str inputs working and clamps a negative
|
|
20
|
+
time to zero instead of emitting a negative-component SRT stamp.
|
|
21
|
+
"""
|
|
22
|
+
seconds = max(0.0, float(seconds))
|
|
23
|
+
total_ms = int(Decimal(str(seconds)) * 1000)
|
|
24
|
+
h, remainder = divmod(total_ms, 3_600_000)
|
|
25
|
+
m, remainder = divmod(remainder, 60_000)
|
|
26
|
+
s, ms = divmod(remainder, 1000)
|
|
21
27
|
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
|
|
22
28
|
|
|
23
29
|
|
|
@@ -45,6 +51,7 @@ def _subtitle_style_config(canvas=None):
|
|
|
45
51
|
"""
|
|
46
52
|
style = {
|
|
47
53
|
"font_name": CONFIG["subtitle_font_name"],
|
|
54
|
+
"font_file": CONFIG["subtitle_font_file"],
|
|
48
55
|
"font_size": CONFIG["subtitle_font_size"],
|
|
49
56
|
"primary_color": CONFIG["subtitle_primary_color"],
|
|
50
57
|
"outline_color": CONFIG["subtitle_outline_color"],
|
|
@@ -278,7 +285,7 @@ def _subtitle_entries(narration):
|
|
|
278
285
|
max_chars = CONFIG["subtitle_max_chars"]
|
|
279
286
|
entries = []
|
|
280
287
|
for seg in narration:
|
|
281
|
-
text = seg
|
|
288
|
+
text = seg["spoken_text"]
|
|
282
289
|
start, end = float(seg["actual_place_start"]), float(seg["actual_place_end"])
|
|
283
290
|
entries.extend(_distribute_chunks(_split_subtitle_chunks(text, max_chars), start, end))
|
|
284
291
|
return entries
|
package/lib/video-recap/skills/video-assemble/scripts/{subtitle_render.py → subtitles/render.py}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""SRT/ASS serialization for narration and original-dialogue subtitles."""
|
|
2
2
|
|
|
3
3
|
from source_subtitles import _combined_subtitle_entries
|
|
4
|
-
from
|
|
4
|
+
from subtitles.core import (
|
|
5
5
|
_normalize_subtitle_text,
|
|
6
6
|
_seconds_to_ass_time,
|
|
7
7
|
_seconds_to_srt_time,
|
|
@@ -16,7 +16,7 @@ def _generate_srt(narration, work_dir, video_duration):
|
|
|
16
16
|
for idx, entry in enumerate(_combined_subtitle_entries(narration, work_dir, video_duration), start=1):
|
|
17
17
|
srt_lines.append(str(idx))
|
|
18
18
|
srt_lines.append(f"{_seconds_to_srt_time(entry['start'])} --> {_seconds_to_srt_time(entry['end'])}")
|
|
19
|
-
srt_lines.append(_normalize_subtitle_text(entry["text"]))
|
|
19
|
+
srt_lines.append(entry["text"] if entry.get("_bound_track") else _normalize_subtitle_text(entry["text"]))
|
|
20
20
|
srt_lines.append("")
|
|
21
21
|
srt_path = work_dir / "subtitles.srt"
|
|
22
22
|
srt_path.write_text("\n".join(srt_lines), encoding="utf-8")
|
|
@@ -69,10 +69,11 @@ def _generate_ass(narration, work_dir, video_duration, canvas):
|
|
|
69
69
|
]
|
|
70
70
|
# entries are already split into short one-line chunks, so no wrapping here.
|
|
71
71
|
for entry in _combined_subtitle_entries(narration, work_dir, video_duration):
|
|
72
|
-
text = _escape_ass_text(_normalize_subtitle_text(entry["text"]))
|
|
72
|
+
text = _escape_ass_text(entry["text"] if entry.get("_bound_track") else _normalize_subtitle_text(entry["text"]))
|
|
73
73
|
ass_lines.append(
|
|
74
74
|
"Dialogue: 0,"
|
|
75
|
-
f"{
|
|
75
|
+
f"{entry.get('ass_start', _seconds_to_ass_time(entry['start']))},"
|
|
76
|
+
f"{entry.get('ass_end', _seconds_to_ass_time(entry['end']))},"
|
|
76
77
|
f"Default,,0,0,0,,{text}"
|
|
77
78
|
)
|
|
78
79
|
|