@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,324 @@
|
|
|
1
|
+
"""Strict loader for versioned, output-clock subtitle tracks.
|
|
2
|
+
|
|
3
|
+
This module intentionally uses only the Python standard library so it can be
|
|
4
|
+
loaded by the independently distributed ``video-assemble`` skill.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from fractions import Fraction
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
# Digest keys older tracks declared; they are ignored, never a reason to fail.
|
|
20
|
+
_LEGACY_BINDING_KEYS = frozenset({"sha256", "edit_sha256"})
|
|
21
|
+
_EVIDENCE_KINDS = frozenset(
|
|
22
|
+
{
|
|
23
|
+
"human_verified",
|
|
24
|
+
"word_timestamps",
|
|
25
|
+
"asr_boundary_calibrated",
|
|
26
|
+
"legacy_estimate",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
_CALIBRATION_KINDS = frozenset({"none", "asr_energy", "human_boundary"})
|
|
30
|
+
_WORD_ALIGNMENT_KINDS = frozenset({"none", "asr_words", "human_words"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SubtitleTrackError(ValueError):
|
|
34
|
+
"""The subtitle track is malformed, stale, or incompatible with its caller."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _fail(path, message):
|
|
38
|
+
raise SubtitleTrackError(f"{path}: {message}")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _mapping(value, path):
|
|
42
|
+
if not isinstance(value, Mapping):
|
|
43
|
+
_fail(path, "must be an object")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _strict_fields(value, *, required, optional=(), path):
|
|
48
|
+
value = _mapping(value, path)
|
|
49
|
+
keys = set(value)
|
|
50
|
+
missing = set(required) - keys
|
|
51
|
+
unknown = keys - set(required) - set(optional)
|
|
52
|
+
if missing:
|
|
53
|
+
_fail(path, f"missing field(s): {', '.join(sorted(missing))}")
|
|
54
|
+
if unknown:
|
|
55
|
+
_fail(path, f"unknown field(s): {', '.join(sorted(unknown))}")
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _integer(value, path, *, minimum=None):
|
|
60
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
61
|
+
_fail(path, "must be an integer")
|
|
62
|
+
if minimum is not None and value < minimum:
|
|
63
|
+
_fail(path, f"must be >= {minimum}")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _nonempty_string(value, path):
|
|
68
|
+
if not isinstance(value, str) or not value.strip():
|
|
69
|
+
_fail(path, "must be a nonempty string")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _local_path(value, path):
|
|
74
|
+
return str(Path(_nonempty_string(value, path)).resolve())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _seconds_fraction(value, path):
|
|
78
|
+
if isinstance(value, bool):
|
|
79
|
+
_fail(path, "duration must be finite and numeric")
|
|
80
|
+
if isinstance(value, Fraction):
|
|
81
|
+
result = value
|
|
82
|
+
elif isinstance(value, Decimal):
|
|
83
|
+
if not value.is_finite():
|
|
84
|
+
_fail(path, "duration must be finite")
|
|
85
|
+
result = Fraction(value)
|
|
86
|
+
elif isinstance(value, int):
|
|
87
|
+
result = Fraction(value)
|
|
88
|
+
elif isinstance(value, float):
|
|
89
|
+
if not math.isfinite(value):
|
|
90
|
+
_fail(path, "duration must be finite")
|
|
91
|
+
result = Fraction(str(value))
|
|
92
|
+
else:
|
|
93
|
+
_fail(path, "duration must be an int, float, Decimal, or Fraction")
|
|
94
|
+
if result < 0:
|
|
95
|
+
_fail(path, "duration must be nonnegative")
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _load_document(path_or_mapping):
|
|
100
|
+
if isinstance(path_or_mapping, Mapping):
|
|
101
|
+
return path_or_mapping
|
|
102
|
+
try:
|
|
103
|
+
path = Path(path_or_mapping)
|
|
104
|
+
except TypeError as exc:
|
|
105
|
+
raise SubtitleTrackError("track must be a mapping or filesystem path") from exc
|
|
106
|
+
try:
|
|
107
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
108
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
109
|
+
raise SubtitleTrackError(f"cannot read subtitle track {path}: {exc}") from exc
|
|
110
|
+
return _mapping(value, "track")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _validate_picture_binding(binding, expected):
|
|
114
|
+
binding = _strict_fields(
|
|
115
|
+
binding, required={"path"}, optional={"edit_plan"} | _LEGACY_BINDING_KEYS,
|
|
116
|
+
path="bindings.picture",
|
|
117
|
+
)
|
|
118
|
+
expected = _mapping(expected, "expected_picture_identity")
|
|
119
|
+
picture_path = _local_path(binding["path"], "bindings.picture.path")
|
|
120
|
+
expected_path = _local_path(expected.get("path"), "expected_picture_identity.path")
|
|
121
|
+
if picture_path != expected_path:
|
|
122
|
+
_fail("bindings.picture.path", "picture path does not match current picture")
|
|
123
|
+
|
|
124
|
+
edit_plan = binding.get("edit_plan")
|
|
125
|
+
if edit_plan is not None:
|
|
126
|
+
edit_plan = _local_path(edit_plan, "bindings.picture.edit_plan")
|
|
127
|
+
if "edit_plan" not in expected:
|
|
128
|
+
_fail("expected_picture_identity.edit_plan", "edit plan is required by track")
|
|
129
|
+
expected_edit = _local_path(expected["edit_plan"], "expected_picture_identity.edit_plan")
|
|
130
|
+
if edit_plan != expected_edit:
|
|
131
|
+
_fail("bindings.picture.edit_plan", "edit plan does not match current edit")
|
|
132
|
+
return {"path": picture_path, **({"edit_plan": edit_plan} if edit_plan else {})}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
_AUDIO_BINDING_FACTS = ("selected_stream", "sample_rate", "packet_count")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_audio_binding(binding, expected):
|
|
139
|
+
binding = _strict_fields(
|
|
140
|
+
binding, required=set(_AUDIO_BINDING_FACTS), optional=_LEGACY_BINDING_KEYS,
|
|
141
|
+
path="bindings.audio",
|
|
142
|
+
)
|
|
143
|
+
expected = _mapping(expected, "expected_audio_identity")
|
|
144
|
+
result = {}
|
|
145
|
+
for key in _AUDIO_BINDING_FACTS:
|
|
146
|
+
declared = _integer(binding[key], f"bindings.audio.{key}", minimum=0)
|
|
147
|
+
actual = _integer(expected.get(key), f"expected_audio_identity.{key}", minimum=0)
|
|
148
|
+
if declared != actual:
|
|
149
|
+
_fail(f"bindings.audio.{key}", f"{key} does not match adopted audio")
|
|
150
|
+
result[key] = declared
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _validate_evidence(value, cue_path):
|
|
155
|
+
path = f"{cue_path}.timing_evidence"
|
|
156
|
+
value = _strict_fields(
|
|
157
|
+
value,
|
|
158
|
+
required={"kind", "evidence_refs", "calibration", "word_alignment"},
|
|
159
|
+
path=path,
|
|
160
|
+
)
|
|
161
|
+
kind = _nonempty_string(value["kind"], f"{path}.kind")
|
|
162
|
+
calibration = _nonempty_string(value["calibration"], f"{path}.calibration")
|
|
163
|
+
word_alignment = _nonempty_string(
|
|
164
|
+
value["word_alignment"], f"{path}.word_alignment"
|
|
165
|
+
)
|
|
166
|
+
if kind not in _EVIDENCE_KINDS:
|
|
167
|
+
_fail(path, f"timing_evidence kind is not supported: {kind!r}")
|
|
168
|
+
if calibration not in _CALIBRATION_KINDS:
|
|
169
|
+
_fail(path, f"timing_evidence calibration is not supported: {calibration!r}")
|
|
170
|
+
if word_alignment not in _WORD_ALIGNMENT_KINDS:
|
|
171
|
+
_fail(path, f"timing_evidence word_alignment is not supported: {word_alignment!r}")
|
|
172
|
+
refs = value["evidence_refs"]
|
|
173
|
+
if not isinstance(refs, list):
|
|
174
|
+
_fail(path, "timing_evidence evidence_refs must be a list")
|
|
175
|
+
refs = [_nonempty_string(ref, f"{path}.evidence_refs[{index}]") for index, ref in enumerate(refs)]
|
|
176
|
+
|
|
177
|
+
valid_shape = {
|
|
178
|
+
"legacy_estimate": calibration == "none" and word_alignment == "none",
|
|
179
|
+
"asr_boundary_calibrated": calibration == "asr_energy" and word_alignment == "none",
|
|
180
|
+
"word_timestamps": calibration in {"none", "asr_energy"} and word_alignment == "asr_words",
|
|
181
|
+
"human_verified": calibration == "human_boundary" and word_alignment in {"none", "human_words"},
|
|
182
|
+
}[kind]
|
|
183
|
+
if not valid_shape:
|
|
184
|
+
_fail(path, f"timing_evidence fields are inconsistent with kind {kind!r}")
|
|
185
|
+
if kind != "legacy_estimate" and not refs:
|
|
186
|
+
_fail(path, f"timing_evidence kind {kind!r} requires evidence_refs")
|
|
187
|
+
return {
|
|
188
|
+
"kind": kind,
|
|
189
|
+
"evidence_refs": refs,
|
|
190
|
+
"calibration": calibration,
|
|
191
|
+
"word_alignment": word_alignment,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def load_subtitle_track(
|
|
196
|
+
path_or_mapping,
|
|
197
|
+
*,
|
|
198
|
+
expected_picture_identity,
|
|
199
|
+
expected_audio_identity,
|
|
200
|
+
expected_duration_seconds,
|
|
201
|
+
reject_legacy_estimate=False,
|
|
202
|
+
):
|
|
203
|
+
"""Validate schema v1 and return metadata plus second-based render entries.
|
|
204
|
+
|
|
205
|
+
Identity arguments are current facts (paths, stream index, sample rate,
|
|
206
|
+
packet count) supplied independently by the caller; declarations inside the
|
|
207
|
+
track are compared against them, never accepted on their own. Cue boundaries
|
|
208
|
+
and text are validated, not split or corrected.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
if not isinstance(reject_legacy_estimate, bool):
|
|
212
|
+
_fail("reject_legacy_estimate", "must be a boolean")
|
|
213
|
+
track = _strict_fields(
|
|
214
|
+
_load_document(path_or_mapping),
|
|
215
|
+
required={"schema_version", "clock", "overlap_policy", "bindings", "cues"},
|
|
216
|
+
path="track",
|
|
217
|
+
)
|
|
218
|
+
if type(track["schema_version"]) is not int or track["schema_version"] != SCHEMA_VERSION:
|
|
219
|
+
_fail(
|
|
220
|
+
"schema_version",
|
|
221
|
+
f"only schema_version {SCHEMA_VERSION} is supported; got {track['schema_version']!r}",
|
|
222
|
+
)
|
|
223
|
+
if track["overlap_policy"] != "forbid":
|
|
224
|
+
_fail("overlap_policy", "schema v1 requires fail-closed value 'forbid'")
|
|
225
|
+
|
|
226
|
+
clock = _strict_fields(
|
|
227
|
+
track["clock"],
|
|
228
|
+
required={"kind", "timebase", "duration_ticks"},
|
|
229
|
+
path="clock",
|
|
230
|
+
)
|
|
231
|
+
if clock["kind"] != "output":
|
|
232
|
+
_fail("clock.kind", "schema v1 supports output clock only")
|
|
233
|
+
timebase = _strict_fields(
|
|
234
|
+
clock["timebase"], required={"numerator", "denominator"}, path="clock.timebase"
|
|
235
|
+
)
|
|
236
|
+
numerator = _integer(timebase["numerator"], "clock.timebase.numerator", minimum=1)
|
|
237
|
+
denominator = _integer(timebase["denominator"], "clock.timebase.denominator", minimum=1)
|
|
238
|
+
duration_ticks = _integer(clock["duration_ticks"], "clock.duration_ticks", minimum=0)
|
|
239
|
+
tick_seconds = Fraction(numerator, denominator)
|
|
240
|
+
duration = duration_ticks * tick_seconds
|
|
241
|
+
expected_duration = _seconds_fraction(expected_duration_seconds, "expected_duration_seconds")
|
|
242
|
+
if duration != expected_duration:
|
|
243
|
+
_fail(
|
|
244
|
+
"clock.duration_ticks",
|
|
245
|
+
f"duration {duration} does not match independently supplied duration {expected_duration}",
|
|
246
|
+
)
|
|
247
|
+
try:
|
|
248
|
+
duration_seconds = float(duration)
|
|
249
|
+
except OverflowError:
|
|
250
|
+
_fail("clock.duration_ticks", "duration must be representable as finite renderer seconds")
|
|
251
|
+
if not math.isfinite(duration_seconds):
|
|
252
|
+
_fail("clock.duration_ticks", "duration must be representable as finite renderer seconds")
|
|
253
|
+
|
|
254
|
+
bindings = _strict_fields(
|
|
255
|
+
track["bindings"], required={"picture", "audio"}, path="bindings"
|
|
256
|
+
)
|
|
257
|
+
picture = _validate_picture_binding(bindings["picture"], expected_picture_identity)
|
|
258
|
+
audio = _validate_audio_binding(bindings["audio"], expected_audio_identity)
|
|
259
|
+
|
|
260
|
+
cues = track["cues"]
|
|
261
|
+
if not isinstance(cues, list):
|
|
262
|
+
_fail("cues", "must be a list")
|
|
263
|
+
entries = []
|
|
264
|
+
kinds = set()
|
|
265
|
+
previous_end = 0
|
|
266
|
+
for index, raw_cue in enumerate(cues):
|
|
267
|
+
cue_path = f"cues[{index}]"
|
|
268
|
+
cue = _strict_fields(
|
|
269
|
+
raw_cue,
|
|
270
|
+
required={"start_tick", "end_tick", "text", "attribution", "timing_evidence"},
|
|
271
|
+
path=cue_path,
|
|
272
|
+
)
|
|
273
|
+
start_tick = _integer(cue["start_tick"], f"{cue_path}.start_tick", minimum=0)
|
|
274
|
+
end_tick = _integer(cue["end_tick"], f"{cue_path}.end_tick", minimum=0)
|
|
275
|
+
if end_tick <= start_tick:
|
|
276
|
+
_fail(cue_path, "half-open cue requires end_tick > start_tick")
|
|
277
|
+
if end_tick > duration_ticks:
|
|
278
|
+
_fail(cue_path, "cue end_tick exceeds output duration")
|
|
279
|
+
if start_tick < previous_end:
|
|
280
|
+
_fail(cue_path, "cue is out of order or overlaps the previous cue")
|
|
281
|
+
previous_end = end_tick
|
|
282
|
+
text = _nonempty_string(cue["text"], f"{cue_path}.text")
|
|
283
|
+
|
|
284
|
+
attribution = _strict_fields(
|
|
285
|
+
cue["attribution"], required={"kind", "ref"}, path=f"{cue_path}.attribution"
|
|
286
|
+
)
|
|
287
|
+
source = _nonempty_string(attribution["kind"], f"{cue_path}.attribution.kind")
|
|
288
|
+
if source not in {"source", "narration"}:
|
|
289
|
+
_fail(f"{cue_path}.attribution.kind", "must be 'source' or 'narration'")
|
|
290
|
+
source_ref = _nonempty_string(attribution["ref"], f"{cue_path}.attribution.ref")
|
|
291
|
+
evidence = _validate_evidence(cue["timing_evidence"], cue_path)
|
|
292
|
+
if reject_legacy_estimate and evidence["kind"] == "legacy_estimate":
|
|
293
|
+
_fail(cue_path, "legacy_estimate is rejected by strict caller policy")
|
|
294
|
+
start_seconds = float(start_tick * tick_seconds)
|
|
295
|
+
end_seconds = float(end_tick * tick_seconds)
|
|
296
|
+
if not (math.isfinite(start_seconds) and math.isfinite(end_seconds)):
|
|
297
|
+
_fail(cue_path, "cue float projection must be finite")
|
|
298
|
+
if end_seconds <= start_seconds:
|
|
299
|
+
_fail(cue_path, "distinct cue ticks collapse in renderer float projection")
|
|
300
|
+
kinds.add(evidence["kind"])
|
|
301
|
+
entries.append(
|
|
302
|
+
{
|
|
303
|
+
"start": start_seconds,
|
|
304
|
+
"end": end_seconds,
|
|
305
|
+
"text": text,
|
|
306
|
+
"source": source,
|
|
307
|
+
"source_ref": source_ref,
|
|
308
|
+
"timing_evidence": evidence,
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
metadata = {
|
|
313
|
+
"schema_version": SCHEMA_VERSION,
|
|
314
|
+
"clock": {
|
|
315
|
+
"kind": "output",
|
|
316
|
+
"timebase": {"numerator": numerator, "denominator": denominator},
|
|
317
|
+
"duration_ticks": duration_ticks,
|
|
318
|
+
"duration_seconds": duration_seconds,
|
|
319
|
+
},
|
|
320
|
+
"overlap_policy": "forbid",
|
|
321
|
+
"bindings": {"picture": picture, "audio": audio},
|
|
322
|
+
"timing_evidence_kinds": sorted(kinds),
|
|
323
|
+
}
|
|
324
|
+
return {"metadata": metadata, "entries": entries}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Bind an explicit output-clock subtitle track to media before any consumer uses it.
|
|
2
|
+
|
|
3
|
+
The first render integration deliberately supports adopted AAC only. A future
|
|
4
|
+
newly mixed narration track needs its own final-mix identity, not this input's
|
|
5
|
+
soundtrack. Bound cue timing is a declared decision, not proof of speech onset.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import bisect
|
|
9
|
+
import json
|
|
10
|
+
from fractions import Fraction
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from artifacts import file_identity
|
|
14
|
+
from adoption.frozen_audio import probe_audio_packets
|
|
15
|
+
from lib import run_cmd
|
|
16
|
+
from subtitles.track import load_subtitle_track
|
|
17
|
+
|
|
18
|
+
TRACK = 'subtitle_track.json'
|
|
19
|
+
VALIDATION = 'subtitle_track_validation.json'
|
|
20
|
+
VALIDATION_SCHEMA = 1
|
|
21
|
+
PROJECTOR_VERSION = 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def current_bindings(video, selected_audio_stream=0, *, edit_plan_path=None):
|
|
25
|
+
"""Compute the binding facts independently of the supplied subtitle track.
|
|
26
|
+
|
|
27
|
+
The picture binding is the resolved media path (plus the edit plan path when one
|
|
28
|
+
exists); the audio binding is the selected stream ordinal with its sample rate and
|
|
29
|
+
packet count.
|
|
30
|
+
"""
|
|
31
|
+
packets = probe_audio_packets(video, selected_audio_stream)
|
|
32
|
+
picture = {'path': str(Path(video).resolve())}
|
|
33
|
+
if edit_plan_path is not None:
|
|
34
|
+
picture['edit_plan'] = str(Path(edit_plan_path).resolve())
|
|
35
|
+
return {'picture': picture,
|
|
36
|
+
'audio': {'selected_stream': selected_audio_stream,
|
|
37
|
+
'sample_rate': packets['sample_rate'],
|
|
38
|
+
'packet_count': packets['packet_count']}}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _picture_clock(video):
|
|
42
|
+
result = run_cmd([
|
|
43
|
+
'ffprobe', '-v', 'error', '-select_streams', 'v:0', '-show_streams', '-show_frames',
|
|
44
|
+
'-show_entries', 'stream=time_base,start_pts,duration_ts:frame=pts', '-of', 'json', str(video),
|
|
45
|
+
])
|
|
46
|
+
if result.returncode:
|
|
47
|
+
raise ValueError(f'Cannot verify subtitle picture clock: {result.stderr}')
|
|
48
|
+
data = json.loads(result.stdout)
|
|
49
|
+
stream = data['streams'][0]
|
|
50
|
+
timebase = Fraction(stream['time_base'])
|
|
51
|
+
if int(stream['start_pts']) != 0:
|
|
52
|
+
raise ValueError('Bound subtitle render requires an output-clock video starting at zero')
|
|
53
|
+
duration = int(stream['duration_ts']) * timebase
|
|
54
|
+
pts = [int(frame['pts']) * timebase for frame in data['frames']]
|
|
55
|
+
if not pts or pts[0] != 0 or any(b <= a for a, b in zip(pts, pts[1:])) or pts[-1] >= duration:
|
|
56
|
+
raise ValueError('Cannot verify picture frame clock: expected strictly increasing PTS within duration')
|
|
57
|
+
return pts, duration
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _project_boundary(target, frame_pts, duration):
|
|
61
|
+
"""Choose an ASS centisecond that changes on the first frame at/after a cue.
|
|
62
|
+
|
|
63
|
+
ASS's 10ms clock is coarser than most frame clocks and can otherwise round a cue
|
|
64
|
+
forward by one frame. Do not silently degrade if two relevant frames cannot be
|
|
65
|
+
separated in that clock.
|
|
66
|
+
libass receives presentation time in integer milliseconds from ffmpeg.
|
|
67
|
+
"""
|
|
68
|
+
index = bisect.bisect_left(frame_pts, target)
|
|
69
|
+
chosen = frame_pts[index] if index < len(frame_pts) else duration
|
|
70
|
+
centiseconds = chosen * 100 // 1
|
|
71
|
+
threshold_ms = int(centiseconds) * 10
|
|
72
|
+
previous_ms = int(frame_pts[index - 1] * 1000) if index else -1
|
|
73
|
+
if threshold_ms <= previous_ms:
|
|
74
|
+
raise ValueError('ASS 10ms clock cannot represent this cue boundary; use a frame-capable renderer')
|
|
75
|
+
ass_time = f'{centiseconds // 360000}:{centiseconds // 6000 % 60:02d}:{centiseconds // 100 % 60:02d}.{centiseconds % 100:02d}'
|
|
76
|
+
return {'requested_time': str(target), 'frame_index': index, 'pts': str(chosen),
|
|
77
|
+
'delta': str(chosen - target), 'ass_time': ass_time}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def prepare_subtitle_track(input_video, work_dir, video_duration, *, audio_mode,
|
|
81
|
+
selected_audio_stream=0, edit_plan_path=None,
|
|
82
|
+
reject_legacy_estimate=False):
|
|
83
|
+
"""Validate and project an explicit track before SRT/ASS/timeline/QC consumers.
|
|
84
|
+
|
|
85
|
+
No track preserves legacy behavior. A present but invalid track fails instead
|
|
86
|
+
of falling back to proportional timing. The persisted record is not approval.
|
|
87
|
+
"""
|
|
88
|
+
work = Path(work_dir)
|
|
89
|
+
path = work / TRACK
|
|
90
|
+
validation = work / VALIDATION
|
|
91
|
+
validation.unlink(missing_ok=True)
|
|
92
|
+
if not path.exists():
|
|
93
|
+
return None
|
|
94
|
+
if audio_mode != 'adopted-packet-copy':
|
|
95
|
+
raise ValueError('Explicit subtitle_track currently requires adopted-packet-copy; new mix is not bound')
|
|
96
|
+
document = json.loads(path.read_bytes())
|
|
97
|
+
bindings = current_bindings(input_video, selected_audio_stream, edit_plan_path=edit_plan_path)
|
|
98
|
+
frame_pts, duration = _picture_clock(input_video)
|
|
99
|
+
if abs(float(duration) - float(video_duration)) > 0.05:
|
|
100
|
+
raise ValueError('Picture duration differs from assembly duration')
|
|
101
|
+
loaded = load_subtitle_track(
|
|
102
|
+
document, expected_picture_identity=bindings['picture'], expected_audio_identity=bindings['audio'],
|
|
103
|
+
expected_duration_seconds=duration, reject_legacy_estimate=reject_legacy_estimate,
|
|
104
|
+
)
|
|
105
|
+
clock = document['clock']['timebase']
|
|
106
|
+
tick = Fraction(clock['numerator'], clock['denominator'])
|
|
107
|
+
for entry, cue in zip(loaded['entries'], document['cues']):
|
|
108
|
+
start = _project_boundary(cue['start_tick'] * tick, frame_pts, duration)
|
|
109
|
+
end = _project_boundary(cue['end_tick'] * tick, frame_pts, duration)
|
|
110
|
+
if end['frame_index'] <= start['frame_index']:
|
|
111
|
+
raise ValueError('Subtitle cue has no visible frame in the actual picture clock')
|
|
112
|
+
entry.update(_bound_track=True, ass_start=start['ass_time'], ass_end=end['ass_time'],
|
|
113
|
+
start=float(Fraction(start['pts'])), end=float(Fraction(end['pts'])))
|
|
114
|
+
entry['frame_projection'] = {'requested_ticks': [cue['start_tick'], cue['end_tick']],
|
|
115
|
+
'start': start, 'end': end}
|
|
116
|
+
loaded['validation_schema'] = VALIDATION_SCHEMA
|
|
117
|
+
loaded['projector_version'] = PROJECTOR_VERSION
|
|
118
|
+
loaded['binding'] = {
|
|
119
|
+
'input_video': str(Path(input_video).resolve()), 'identities': bindings,
|
|
120
|
+
'inputs': {'track': file_identity(path), 'video': file_identity(input_video),
|
|
121
|
+
'edit_plan': file_identity(edit_plan_path) if edit_plan_path is not None else None},
|
|
122
|
+
'duration': str(duration), 'frame_count': len(frame_pts),
|
|
123
|
+
'edit_plan': str(Path(edit_plan_path).resolve()) if edit_plan_path is not None else None,
|
|
124
|
+
'verification': 'media_binding_and_declared_frame_projection_only',
|
|
125
|
+
'direct_listening': 'NOT_CHECKED', 'acoustic_alignment': 'NOT_CHECKED',
|
|
126
|
+
}
|
|
127
|
+
validation.write_text(json.dumps(loaded, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
|
128
|
+
return loaded
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _load_validation(work):
|
|
132
|
+
path = Path(work) / VALIDATION
|
|
133
|
+
if not path.exists():
|
|
134
|
+
raise ValueError('Explicit subtitle track must be prepared against current media before use')
|
|
135
|
+
loaded = json.loads(path.read_text(encoding='utf-8'))
|
|
136
|
+
if (type(loaded.get('validation_schema')) is not int or loaded['validation_schema'] != VALIDATION_SCHEMA
|
|
137
|
+
or type(loaded.get('projector_version')) is not int or loaded['projector_version'] != PROJECTOR_VERSION):
|
|
138
|
+
raise ValueError('Subtitle validation/projector version changed; prepare again')
|
|
139
|
+
return loaded
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def bound_subtitle_entries(work_dir, video_duration):
|
|
143
|
+
"""Return prepared exact entries, or None only when no explicit track exists."""
|
|
144
|
+
if work_dir is None:
|
|
145
|
+
return None
|
|
146
|
+
work = Path(work_dir)
|
|
147
|
+
if not (work / TRACK).exists():
|
|
148
|
+
return None
|
|
149
|
+
loaded = _load_validation(work)
|
|
150
|
+
record = loaded['binding']
|
|
151
|
+
inputs = record['inputs']
|
|
152
|
+
if file_identity(work / TRACK) != inputs['track']:
|
|
153
|
+
raise ValueError('stale subtitle track: author file changed after prepare')
|
|
154
|
+
video = Path(record['input_video'])
|
|
155
|
+
if not video.is_file() or file_identity(video) != inputs['video']:
|
|
156
|
+
raise ValueError('stale subtitle track: adopted media changed after prepare')
|
|
157
|
+
if record['edit_plan'] and (not Path(record['edit_plan']).is_file()
|
|
158
|
+
or file_identity(record['edit_plan']) != inputs['edit_plan']):
|
|
159
|
+
raise ValueError('stale subtitle track: edit plan changed after prepare')
|
|
160
|
+
if abs(float(Fraction(record['duration'])) - float(video_duration)) > 0.05:
|
|
161
|
+
raise ValueError('stale subtitle track: consumer duration changed after prepare')
|
|
162
|
+
return loaded['entries']
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def verify_rendered_picture(work_dir, output_path):
|
|
166
|
+
"""Check the actual rendered frame clock, not just pre-render declarations."""
|
|
167
|
+
work = Path(work_dir)
|
|
168
|
+
if not (work / TRACK).exists():
|
|
169
|
+
return None
|
|
170
|
+
path = work / VALIDATION
|
|
171
|
+
loaded = json.loads(path.read_text(encoding='utf-8'))
|
|
172
|
+
bound_subtitle_entries(work, float(Fraction(loaded['binding']['duration'])))
|
|
173
|
+
loaded['rendered_picture'] = {'output': str(Path(output_path).resolve()),
|
|
174
|
+
'frame_clock_verified': False}
|
|
175
|
+
path.write_text(json.dumps(loaded, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
|
176
|
+
if _picture_clock(output_path) != _picture_clock(loaded['binding']['input_video']):
|
|
177
|
+
raise ValueError('Rendered frame clock changed; subtitle frame projection is no longer valid')
|
|
178
|
+
loaded['rendered_picture'].update(frame_clock_verified=True,
|
|
179
|
+
frame_count=loaded['binding']['frame_count'],
|
|
180
|
+
identity=file_identity(output_path))
|
|
181
|
+
path.write_text(json.dumps(loaded, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
|
182
|
+
return loaded['rendered_picture']
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def manifest_subtitle_evidence(work_dir, input_video, output_path):
|
|
186
|
+
"""Only expose evidence for the actual input and verified output of this render."""
|
|
187
|
+
work = Path(work_dir)
|
|
188
|
+
if not (work / TRACK).exists():
|
|
189
|
+
return None
|
|
190
|
+
loaded = _load_validation(work)
|
|
191
|
+
bound_subtitle_entries(work, float(Fraction(loaded['binding']['duration'])))
|
|
192
|
+
rendered = loaded.get('rendered_picture', {})
|
|
193
|
+
if rendered.get('frame_clock_verified') is not True:
|
|
194
|
+
raise ValueError('Subtitle output frame clock has not been verified')
|
|
195
|
+
if str(Path(input_video).resolve()) != loaded['binding']['input_video']:
|
|
196
|
+
raise ValueError('stale subtitle manifest: input media differs')
|
|
197
|
+
if not Path(output_path).is_file() or file_identity(output_path) != rendered.get('identity'):
|
|
198
|
+
raise ValueError('stale subtitle manifest: actual output differs from verified render')
|
|
199
|
+
return {'validation_path': str((work / VALIDATION).resolve()),
|
|
200
|
+
'binding': loaded['binding'], 'metadata': loaded['metadata'], 'rendered_picture': rendered}
|
|
@@ -36,6 +36,12 @@ def _ceil_time(value, digits=4):
|
|
|
36
36
|
return math.ceil((float(value) * scale) - 1e-9) / scale
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def _floor_time(value, digits=4):
|
|
40
|
+
"""Round an interval start outward so serialization cannot clip source samples."""
|
|
41
|
+
scale = 10 ** digits
|
|
42
|
+
return math.floor((float(value) * scale) + 1e-9) / scale
|
|
43
|
+
|
|
44
|
+
|
|
39
45
|
def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
40
46
|
bgm=None, ducking=None, subtitle_segments=None,
|
|
41
47
|
image_segments=(), resource_packages=None,
|
|
@@ -48,15 +54,17 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
48
54
|
"timeline_start", "timeline_end"}] (cut mode: one per clip;
|
|
49
55
|
full mode: a single clip spanning the whole video).
|
|
50
56
|
narration_segments: placed beats [{"source_path", "timeline_start",
|
|
51
|
-
"timeline_end", "text", "overlaps_speech", "gain"
|
|
57
|
+
"timeline_end", "text", "overlaps_speech", "gain"}]; a zero-width
|
|
52
58
|
beat (an unplaced segment) is skipped.
|
|
53
59
|
subtitle_segments: optional display-ready text cues [{"text", "timeline_start",
|
|
54
60
|
"timeline_end"}]. When present, this is authoritative for the
|
|
55
61
|
subtitle/text track; narration segment text remains raw editor metadata.
|
|
56
|
-
bgm: optional {"source_path", "volume", "ducking_volume"}.
|
|
57
|
-
ducking: {"idle", "speech", "quiet", "fade", "bridge"
|
|
62
|
+
bgm: optional {"source_path", "volume", "ducking_volume", "fade"}.
|
|
63
|
+
ducking: {"idle", "speech", "quiet", "fade", "bridge"} for the original-audio
|
|
58
64
|
automation; None disables original ducking (flat original). `bridge` holds
|
|
59
|
-
the duck across inter-beat gaps shorter than it
|
|
65
|
+
the duck across inter-beat gaps shorter than it.
|
|
66
|
+
image_segments: optional v2 local image overlays [{"source_path", "timeline_start",
|
|
67
|
+
"timeline_end", ...authoring extensions}], passed through as authored.
|
|
60
68
|
"""
|
|
61
69
|
placed = [
|
|
62
70
|
s for s in narration_segments
|
|
@@ -71,7 +79,7 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
71
79
|
restore_at = max(
|
|
72
80
|
hold_end, float(s.get("source_restore_at", hold_end + float(ducking["fade"])))
|
|
73
81
|
)
|
|
74
|
-
level = float(ducking["speech" if s
|
|
82
|
+
level = float(ducking["speech" if s["overlaps_speech"] else "quiet"])
|
|
75
83
|
duck_windows.append((float(s["timeline_start"]), hold_end, level, restore_at))
|
|
76
84
|
|
|
77
85
|
# --- video track: each clip carries its original audio + ducking automation
|
|
@@ -82,7 +90,7 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
82
90
|
if ducking is not None:
|
|
83
91
|
audio["volume_keyframes"] = release_ducking_keyframes(
|
|
84
92
|
duck_windows, ducking["idle"], ducking["fade"], ts, te,
|
|
85
|
-
bridge=ducking
|
|
93
|
+
bridge=ducking["bridge"])
|
|
86
94
|
audio["base_gain"] = round(float(ducking["idle"]), 4)
|
|
87
95
|
else:
|
|
88
96
|
audio["base_gain"] = 1.0
|
|
@@ -110,11 +118,11 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
110
118
|
for s in placed:
|
|
111
119
|
narration = {
|
|
112
120
|
"source_path": s["source_path"],
|
|
113
|
-
"timeline_start":
|
|
121
|
+
"timeline_start": _floor_time(s["timeline_start"], 4),
|
|
114
122
|
"timeline_end": _ceil_time(s["timeline_end"], 4),
|
|
115
|
-
"gain": round(float(s
|
|
116
|
-
"text": s
|
|
117
|
-
"overlaps_speech": bool(s
|
|
123
|
+
"gain": round(float(s["gain"]), 4),
|
|
124
|
+
"text": s["text"],
|
|
125
|
+
"overlaps_speech": bool(s["overlaps_speech"]),
|
|
118
126
|
}
|
|
119
127
|
for key in ("source_duck_end", "source_restore_at", "source_handoff_status", "source_entry_status"):
|
|
120
128
|
if key in s:
|
|
@@ -127,12 +135,12 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
127
135
|
"segments": narr_segs})
|
|
128
136
|
|
|
129
137
|
# --- bgm track (optional, looped, ducked under narration)
|
|
130
|
-
if bgm
|
|
131
|
-
base = float(bgm
|
|
132
|
-
duck = float(bgm
|
|
133
|
-
fade = float(bgm
|
|
138
|
+
if bgm:
|
|
139
|
+
base = float(bgm["volume"])
|
|
140
|
+
duck = float(bgm["ducking_volume"])
|
|
141
|
+
fade = float(bgm["fade"])
|
|
134
142
|
kfs = ducking_keyframes(windows, base, duck, fade, 0.0, duration_s,
|
|
135
|
-
bridge=
|
|
143
|
+
bridge=ducking["bridge"] if ducking else None)
|
|
136
144
|
tracks.append({
|
|
137
145
|
"kind": "audio", "name": "bgm", "role": "bgm", "loop": True,
|
|
138
146
|
"segments": [{
|
|
@@ -165,41 +173,17 @@ def build_timeline(canvas, duration_s, video_clips, narration_segments,
|
|
|
165
173
|
if text_segs:
|
|
166
174
|
tracks.append({"kind": "text", "name": "subtitle", "segments": text_segs})
|
|
167
175
|
|
|
168
|
-
# --- local image overlays (optional, timeline schema v2)
|
|
176
|
+
# --- local image overlays (optional, timeline schema v2). Transform fields are
|
|
177
|
+
# optional authoring extensions; the JianYing exporter validates and defaults them.
|
|
169
178
|
images = []
|
|
170
179
|
for segment in image_segments:
|
|
171
|
-
if not isinstance(segment, dict) or not segment.get("source_path"):
|
|
172
|
-
continue
|
|
173
|
-
try:
|
|
174
|
-
ts = max(0.0, float(segment["timeline_start"]))
|
|
175
|
-
te = min(float(duration_s), float(segment["timeline_end"]))
|
|
176
|
-
except (KeyError, TypeError, ValueError):
|
|
177
|
-
continue
|
|
178
|
-
if not math.isfinite(ts) or not math.isfinite(te) or te <= ts:
|
|
179
|
-
continue
|
|
180
|
-
scale = segment.get("scale") if isinstance(segment.get("scale"), dict) else {}
|
|
181
|
-
position = segment.get("position") if isinstance(segment.get("position"), dict) else {}
|
|
182
|
-
flip = segment.get("flip") if isinstance(segment.get("flip"), dict) else {}
|
|
183
180
|
image = {
|
|
184
|
-
"source_path":
|
|
185
|
-
"timeline_start": round(
|
|
186
|
-
"timeline_end": round(
|
|
187
|
-
"opacity": round(max(0.0, min(1.0, float(segment.get("opacity", 1.0)))), 4),
|
|
188
|
-
"rotation_degrees": round(float(segment.get("rotation_degrees", 0.0)), 4),
|
|
189
|
-
"scale": {
|
|
190
|
-
"x": round(float(scale.get("x", 1.0)), 4),
|
|
191
|
-
"y": round(float(scale.get("y", 1.0)), 4),
|
|
192
|
-
},
|
|
193
|
-
"position": {
|
|
194
|
-
"x": round(float(position.get("x", 0.0)), 4),
|
|
195
|
-
"y": round(float(position.get("y", 0.0)), 4),
|
|
196
|
-
},
|
|
197
|
-
"flip": {
|
|
198
|
-
"horizontal": bool(flip.get("horizontal", False)),
|
|
199
|
-
"vertical": bool(flip.get("vertical", False)),
|
|
200
|
-
},
|
|
181
|
+
"source_path": segment["source_path"],
|
|
182
|
+
"timeline_start": round(float(segment["timeline_start"]), 4),
|
|
183
|
+
"timeline_end": round(float(segment["timeline_end"]), 4),
|
|
201
184
|
}
|
|
202
|
-
for key in ("lut", "mask", "
|
|
185
|
+
for key in ("flip", "lut", "mask", "opacity", "position", "rotation_degrees",
|
|
186
|
+
"scale", "speed", "transition"):
|
|
203
187
|
if key in segment:
|
|
204
188
|
image[key] = deepcopy(segment[key])
|
|
205
189
|
images.append(image)
|