@oh-story/dsh 0.1.8 → 0.1.10
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 +237 -84
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +183 -77
- package/lib/drama/skills/short-drama/SKILL.md +5 -1
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +140 -9
- package/lib/drama/skills/short-drama/scripts/project_tool.py +11 -0
- package/lib/drama/skills/short-drama-assets/references/character-and-look.md +28 -0
- package/lib/drama/skills/short-drama-assets/references/continuity-lock.md +8 -2
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +28 -0
- package/lib/drama/skills/short-drama-edit/SKILL.md +146 -0
- package/lib/drama/skills/short-drama-edit/agents/openai.yaml +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/README.md +60 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/package.json +15 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/remotion.config.ts +6 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Root.tsx +26 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/Subtitles.tsx +88 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/font.ts +46 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/index.ts +4 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/src/schema.ts +32 -0
- package/lib/drama/skills/short-drama-edit/assets/remotion/tsconfig.json +13 -0
- package/lib/drama/skills/short-drama-edit/assets//345/211/252/350/276/221/345/215/225.md +43 -0
- package/lib/drama/skills/short-drama-edit/references/cut-craft.md +38 -0
- package/lib/drama/skills/short-drama-edit/references/delivery-verify.md +29 -0
- package/lib/drama/skills/short-drama-edit/references/generated-footage.md +54 -0
- package/lib/drama/skills/short-drama-edit/references/sound-and-subtitles.md +60 -0
- package/lib/drama/skills/short-drama-edit/references/stage-contract.md +51 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1156 -0
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +520 -0
- package/lib/drama/skills/short-drama-image-prompts/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/image-prompts.md +5 -0
- package/lib/drama/skills/short-drama-image-prompts/assets/lookdev-prompts.md +3 -0
- package/lib/drama/skills/short-drama-image-prompts/references/common-recipe.md +11 -0
- package/lib/drama/skills/short-drama-novel-analyze/references/aggregation-and-entities.md +2 -2
- package/lib/drama/skills/short-drama-novel-analyze/references/chapter-extraction.md +4 -1
- package/lib/drama/skills/short-drama-produce/SKILL.md +29 -4
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +22 -0
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-h3-video.md +13 -12
- package/lib/drama/skills/short-drama-produce/references/providers/minimax-speech.md +41 -0
- package/lib/drama/skills/short-drama-produce/references/providers/seedance.md +7 -0
- package/lib/drama/skills/short-drama-produce/scripts/fixture_adapter.py +18 -1
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +181 -2
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +199 -3
- package/lib/drama/skills/short-drama-review/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-review/references/production-quality-gates.md +7 -0
- package/lib/drama/skills/short-drama-review/references/rubric-visual-motion.md +10 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +12 -1
- package/lib/drama/skills/short-drama-storyboard/assets/keyframe-prompts.md +4 -0
- package/lib/drama/skills/short-drama-storyboard/references/lighting-craft.md +39 -0
- package/lib/drama/skills/short-drama-storyboard/references/shot-craft.md +78 -2
- package/lib/drama/skills/short-drama-storyboard/references/stage-contract.md +2 -0
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +12 -5
- package/lib/drama/skills/short-drama-video-prompts/references/camera-audio-continuity.md +17 -0
- package/lib/drama/skills/short-drama-video-prompts/references/minimax-h3.md +76 -7
- package/lib/drama/skills/short-drama-video-prompts/references/motion-recipe.md +5 -2
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +21 -0
- package/lib/drama/skills/short-drama-video-prompts/references/production-prompt-grammar.md +6 -17
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.0.md +12 -3
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +61 -5
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +3 -3
- package/lib/drama/skills/short-drama-write/SKILL.md +11 -2
- package/lib/drama/skills/short-drama-write/references/screenplay-format.md +6 -0
- package/lib/drama/skills/short-drama-write/references/stage-contract.md +1 -0
- package/lib/index.js +290 -90
- 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 +9 -35
- 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 +17 -35
- 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 +16 -34
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +28 -75
- package/lib/novel-to-game/skills/game-qa/SKILL.md +9 -20
- package/lib/novel-to-game/skills/game-qa/references/test-design-method.md +10 -6
- package/lib/novel-to-game/skills/game-world-design/SKILL.md +19 -32
- 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 +9 -18
- 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 +10 -27
- package/lib/novel-to-game/skills/novel-to-game/references/intake-method.md +5 -6
- package/lib/novel-to-game/skills/novel-to-game/references/pipeline-contract.md +6 -11
- package/lib/oh-story/manifest.json +419 -254
- package/lib/oh-story/roles/chapter-extractor.md +177 -237
- package/lib/oh-story/roles/character-designer.md +1 -1
- package/lib/oh-story/roles/consistency-checker.md +4 -4
- package/lib/oh-story/roles/narrative-writer.md +56 -243
- package/lib/oh-story/roles/story-architect.md +6 -6
- package/lib/oh-story/roles/story-explorer.md +11 -11
- package/lib/oh-story/skills/story/SKILL.md +13 -7
- package/lib/oh-story/skills/story/VERSION +1 -1
- package/lib/oh-story/skills/story/references/author-memory.md +61 -29
- 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 +54 -229
- package/lib/oh-story/skills/story-deslop/references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-deslop/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-deslop/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-deslop/references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-deslop/references/style-resolution.md +27 -0
- 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 +6 -1
- package/lib/oh-story/skills/story-deslop/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-deslop/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-deslop/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-import/SKILL.md +78 -82
- package/lib/oh-story/skills/story-import/references/character-state-reverse.md +1 -1
- package/lib/oh-story/skills/story-import/references/format-and-structure.md +2 -2
- 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 +10 -8
- package/lib/oh-story/skills/story-import/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-import/references/tracking-transaction.md +9 -34
- package/lib/oh-story/skills/story-import/scripts/check-outline-contract.js +197 -15
- package/lib/oh-story/skills/story-import/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-import/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-long-analyze/SKILL.md +91 -258
- package/lib/oh-story/skills/story-long-analyze/references/author-facing.md +245 -0
- package/lib/oh-story/skills/story-long-analyze/references/deconstruction-notes.md +2 -2
- package/lib/oh-story/skills/story-long-analyze/references/inspiration-library.md +105 -0
- package/lib/oh-story/skills/story-long-analyze/references/material-decomposition.md +113 -105
- package/lib/oh-story/skills/story-long-analyze/references/output-templates.md +169 -125
- package/lib/oh-story/skills/story-long-analyze/references/pipeline-ops.md +172 -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/style-profile-generator.md +42 -132
- package/lib/oh-story/skills/story-long-analyze/references/style-profile-protocol.md +5 -5
- 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 +1106 -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 +4 -1
- package/lib/oh-story/skills/story-long-write/SKILL.md +52 -216
- package/lib/oh-story/skills/story-long-write/references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-long-write/references/artifact-protocols.md +72 -12
- package/lib/oh-story/skills/story-long-write/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-long-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-long-write/references/benchmark-recall.md +20 -0
- package/lib/oh-story/skills/story-long-write/references/cross-book-recall.md +26 -0
- package/lib/oh-story/skills/story-long-write/references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-long-write/references/emotional-methods.md +2 -2
- package/lib/oh-story/skills/story-long-write/references/long-chapter-quality.md +6 -15
- package/lib/oh-story/skills/story-long-write/references/long-format.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/outline-structure-theory.md +2 -0
- package/lib/oh-story/skills/story-long-write/references/project-files.md +110 -0
- package/lib/oh-story/skills/story-long-write/references/reader-contract-and-progression.md +12 -4
- package/lib/oh-story/skills/story-long-write/references/reference-index.md +86 -0
- package/lib/oh-story/skills/story-long-write/references/style-combat-face.md +1 -1
- package/lib/oh-story/skills/story-long-write/references/style-craft.md +9 -29
- 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 +27 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-long-write/references/tracking-transaction.md +9 -34
- package/lib/oh-story/skills/story-long-write/references/workflow-chapter.md +88 -118
- package/lib/oh-story/skills/story-long-write/references/workflow-daily.md +55 -119
- package/lib/oh-story/skills/story-long-write/references/workflow-revision.md +15 -11
- package/lib/oh-story/skills/story-long-write/references/workflow-setup.md +73 -54
- package/lib/oh-story/skills/story-long-write/references/writer-prompt-fallback.md +22 -0
- package/lib/oh-story/skills/story-long-write/references/writing-craft.md +63 -4
- 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_writer_prompt.py +531 -0
- package/lib/oh-story/skills/story-long-write/scripts/check-ai-patterns.js +6 -1
- package/lib/oh-story/skills/story-long-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-contract.js +197 -15
- package/lib/oh-story/skills/story-long-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-long-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-long-write/scripts/outline_view.py +285 -0
- package/lib/oh-story/skills/story-long-write/scripts/storyctl.py +58 -0
- package/lib/oh-story/skills/story-long-write/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-long-write/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-long-write/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-review/SKILL.md +80 -87
- package/lib/oh-story/skills/story-review/references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-review/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-review/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-review/references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-review/references/review-quality.md +6 -12
- package/lib/oh-story/skills/story-review/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-review/references/tracking-initialization.md +31 -0
- package/lib/oh-story/skills/story-review/references/tracking-transaction.md +9 -34
- 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 +6 -1
- package/lib/oh-story/skills/story-review/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-review/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-review/scripts/style-whitelist.js +39 -0
- package/lib/oh-story/skills/story-review/scripts/tracking_commit.py +210 -2
- package/lib/oh-story/skills/story-review/scripts/wordcount_core.py +3 -0
- package/lib/oh-story/skills/story-setup/SKILL.md +58 -43
- package/lib/oh-story/skills/story-setup/references/agent-references/anti-ai-writing.md +29 -56
- package/lib/oh-story/skills/story-setup/references/agent-references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/deslop-gates.md +154 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-setup/references/agent-references/format-and-structure.md +2 -2
- 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-quality.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/short-genre-formulas.md +1 -1
- package/lib/oh-story/skills/story-setup/references/agent-references/style-combat-face.md +1 -1
- 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 +27 -0
- package/lib/oh-story/skills/story-setup/references/agent-references/writing-craft.md +63 -4
- package/lib/oh-story/skills/story-setup/references/diagnostics.md +38 -0
- package/lib/oh-story/skills/story-short-analyze/SKILL.md +22 -3
- package/lib/oh-story/skills/story-short-analyze/references/analysis-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-analyze/references/material-decomposition.md +5 -1
- package/lib/oh-story/skills/story-short-analyze/references/source-story-quality.md +2 -2
- package/lib/oh-story/skills/story-short-scan/SKILL.md +5 -1
- package/lib/oh-story/skills/story-short-write/SKILL.md +15 -324
- package/lib/oh-story/skills/story-short-write/references/author-memory.md +61 -29
- package/lib/oh-story/skills/story-short-write/references/banned-words.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/benchmark-recall.md +60 -0
- package/lib/oh-story/skills/story-short-write/references/dialogue-mastery.md +10 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +4 -4
- package/lib/oh-story/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +5 -5
- package/lib/oh-story/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +3 -3
- package/lib/oh-story/skills/story-short-write/references/genre-writing-formulas.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/genre-writing-techniques.md +1 -1
- package/lib/oh-story/skills/story-short-write/references/reference-index.md +55 -0
- package/lib/oh-story/skills/story-short-write/references/short-craft.md +12 -14
- package/lib/oh-story/skills/story-short-write/references/short-deslop.md +10 -10
- package/lib/oh-story/skills/story-short-write/references/short-format.md +3 -1
- package/lib/oh-story/skills/story-short-write/references/short-prose-quality.md +2 -2
- package/lib/oh-story/skills/story-short-write/references/style-resolution.md +27 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-design.md +78 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-draft.md +126 -0
- package/lib/oh-story/skills/story-short-write/references/workflow-revision.md +32 -0
- package/lib/oh-story/skills/story-short-write/references/writing-workflow.md +5 -5
- 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 +6 -1
- package/lib/oh-story/skills/story-short-write/scripts/check-degeneration.js +39 -20
- package/lib/oh-story/skills/story-short-write/scripts/check-delivery-contract.js +34 -5
- package/lib/oh-story/skills/story-short-write/scripts/check-outline-copy.js +90 -23
- package/lib/oh-story/skills/story-short-write/scripts/normalize-punctuation.js +10 -5
- package/lib/oh-story/skills/story-short-write/scripts/style-whitelist.js +39 -0
- 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 +33 -0
- package/lib/types/client/production-runtime.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 +52 -0
- 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/video-recap/CHANGELOG.md +31 -1
- package/lib/video-recap/manifest.json +162 -162
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +4 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +47 -60
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +77 -115
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +41 -40
- package/lib/video-recap/skills/video-assemble/scripts/audio_automation.py +22 -32
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +102 -192
- package/lib/video-recap/skills/video-assemble/scripts/jianying_builders.py +14 -35
- package/lib/video-recap/skills/video-assemble/scripts/jianying_model.py +14 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_optional.py +8 -9
- package/lib/video-recap/skills/video-assemble/scripts/jianying_schema.py +3 -14
- package/lib/video-recap/skills/video-assemble/scripts/jianying_timeline_contract.py +23 -12
- package/lib/video-recap/skills/video-assemble/scripts/jianying_tracks.py +1 -4
- package/lib/video-recap/skills/video-assemble/scripts/jianying_writer.py +24 -25
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +31 -85
- package/lib/video-recap/skills/video-assemble/scripts/media.py +100 -144
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +101 -195
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +10 -21
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +90 -169
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_core.py +69 -122
- package/lib/video-recap/skills/video-assemble/scripts/subtitle_render.py +10 -17
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +33 -48
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +14 -20
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +135 -213
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +49 -57
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +38 -74
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +46 -95
- package/lib/video-recap/skills/video-cut/scripts/lib.py +23 -25
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +70 -173
- package/lib/video-recap/skills/video-cut/scripts/narration_mapping.py +28 -53
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +162 -322
- package/lib/video-recap/skills/video-recap/references/timeline-and-jianying.md +6 -7
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +104 -124
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +255 -412
- package/lib/video-recap/skills/video-recap/scripts/lib.py +30 -52
- package/lib/video-recap/skills/video-recap/scripts/materials.py +97 -120
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_client.py +7 -12
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_evidence.py +46 -97
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_observations.py +18 -36
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_payload.py +29 -47
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_report.py +43 -76
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc_runner.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +2 -1
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +2 -2
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +129 -145
- package/lib/video-recap/skills/video-recap/scripts/recap_review.py +27 -20
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +20 -31
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +31 -106
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +43 -113
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +196 -377
- package/lib/video-recap/skills/video-script/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-script/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-script/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-script/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/lib.py +30 -26
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-script/scripts/review_response.py +12 -42
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +0 -6
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/agent_brief.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +63 -172
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +1 -2
- package/lib/video-recap/skills/video-understanding/scripts/brief_context.py +80 -135
- package/lib/video-recap/skills/video-understanding/scripts/brief_inputs.py +125 -167
- package/lib/video-recap/skills/video-understanding/scripts/brief_timeline.py +93 -208
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +1 -1
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +20 -46
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +31 -26
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +145 -140
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +39 -100
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +33 -124
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +1 -4
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +7 -13
- package/lib/video-recap/skills/video-voiceover/scripts/fish_audio.py +12 -30
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +98 -131
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +17 -28
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +151 -240
- package/package.json +45 -45
- package/lib/novel-to-game/skills/game-world-design/references/dialogue-design-method.md +0 -59
- 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 -42
- package/lib/oh-story/skills/story-long-write/references/state-tracking.md +0 -108
|
@@ -1,43 +1,22 @@
|
|
|
1
1
|
"""Snap clip boundaries to complete speech and clean shot transitions."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
|
|
5
4
|
import re
|
|
6
|
-
|
|
7
5
|
import subprocess
|
|
8
|
-
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
|
|
11
8
|
from lib import log
|
|
12
9
|
|
|
13
10
|
|
|
14
|
-
def
|
|
15
|
-
"""
|
|
16
|
-
windows = []
|
|
17
|
-
for w in silence_periods or []:
|
|
18
|
-
if not isinstance(w, dict):
|
|
19
|
-
continue
|
|
20
|
-
try:
|
|
21
|
-
start = float(w.get("start"))
|
|
22
|
-
end = float(w.get("end"))
|
|
23
|
-
except (TypeError, ValueError):
|
|
24
|
-
continue
|
|
25
|
-
if end >= start:
|
|
26
|
-
windows.append({"start": start, "end": end})
|
|
27
|
-
return sorted(windows, key=lambda w: (w["start"], w["end"]))
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _source_artifact_candidates(
|
|
31
|
-
work_dir, filename, source_id=None, source_work_dir=None
|
|
32
|
-
):
|
|
11
|
+
def _find_source_artifact(work_dir, filename, source_id=None, source_work_dir=None):
|
|
12
|
+
"""First existing copy of a per-source understanding artifact, by layout precedence."""
|
|
33
13
|
candidates = []
|
|
34
14
|
if source_work_dir:
|
|
35
15
|
candidates.append(Path(work_dir) / source_work_dir / filename)
|
|
36
|
-
if source_id not
|
|
37
|
-
candidates.append(Path(work_dir) / "sources" /
|
|
16
|
+
if source_id is not None:
|
|
17
|
+
candidates.append(Path(work_dir) / "sources" / source_id / filename)
|
|
38
18
|
candidates.append(Path(work_dir) / filename)
|
|
39
|
-
|
|
40
|
-
return list(dict.fromkeys(candidates))
|
|
19
|
+
return next((path for path in candidates if path.exists()), None)
|
|
41
20
|
|
|
42
21
|
|
|
43
22
|
def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=None):
|
|
@@ -47,112 +26,69 @@ def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=No
|
|
|
47
26
|
after the final spoken sample. Any cut within that closed interval preserves the sentence.
|
|
48
27
|
Low-confidence anchors are deliberately excluded from the hard-safety path.
|
|
49
28
|
"""
|
|
50
|
-
|
|
29
|
+
path = _find_source_artifact(
|
|
51
30
|
work_dir, "speech_boundary_anchors.json", source_id, source_work_dir
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
end = float(anchor.get("time"))
|
|
69
|
-
start = float(anchor.get("pause_start", end))
|
|
70
|
-
except (TypeError, ValueError):
|
|
71
|
-
continue
|
|
72
|
-
start = min(start, end)
|
|
73
|
-
windows.append(
|
|
74
|
-
{
|
|
75
|
-
"start": round(max(0.0, start), 3),
|
|
76
|
-
"end": round(max(0.0, end), 3),
|
|
77
|
-
"kind": "sentence_anchor",
|
|
78
|
-
"confidence": confidence,
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
return sorted(windows, key=lambda row: (row["start"], row["end"]))
|
|
82
|
-
return []
|
|
31
|
+
)
|
|
32
|
+
if path is None:
|
|
33
|
+
return []
|
|
34
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
35
|
+
windows = [
|
|
36
|
+
{
|
|
37
|
+
"start": round(anchor["pause_start"], 3),
|
|
38
|
+
"end": round(anchor["time"], 3),
|
|
39
|
+
"kind": "sentence_anchor",
|
|
40
|
+
"confidence": anchor["confidence"],
|
|
41
|
+
}
|
|
42
|
+
for anchor in payload["sentence_anchors"]
|
|
43
|
+
if anchor["confidence"] in {"high", "medium"}
|
|
44
|
+
]
|
|
45
|
+
return sorted(windows, key=lambda row: (row["start"], row["end"]))
|
|
83
46
|
|
|
84
47
|
|
|
85
48
|
def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
|
|
86
|
-
"""
|
|
87
|
-
|
|
49
|
+
"""Merged ASR speech spans (asr_clean.json wins over asr_result.json).
|
|
50
|
+
|
|
51
|
+
Only used to decide whether an unsafe edge blocks; a missing transcript means unchecked.
|
|
52
|
+
"""
|
|
53
|
+
rows = []
|
|
88
54
|
for filename in ("asr_clean.json", "asr_result.json"):
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if
|
|
93
|
-
continue
|
|
94
|
-
try:
|
|
95
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
96
|
-
except (OSError, ValueError):
|
|
97
|
-
payload = None
|
|
98
|
-
if payload is not None:
|
|
99
|
-
break
|
|
100
|
-
if payload is not None:
|
|
55
|
+
path = _find_source_artifact(work_dir, filename, source_id, source_work_dir)
|
|
56
|
+
if path is not None:
|
|
57
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
58
|
+
rows = payload["segments"] if filename == "asr_clean.json" else payload
|
|
101
59
|
break
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if not isinstance(row, dict) or not str(row.get("text") or "").strip():
|
|
107
|
-
continue
|
|
108
|
-
try:
|
|
109
|
-
start, end = float(row.get("start")), float(row.get("end"))
|
|
110
|
-
except (TypeError, ValueError):
|
|
111
|
-
continue
|
|
112
|
-
if end > start:
|
|
113
|
-
spans.append({"start": max(0.0, start), "end": end})
|
|
60
|
+
spans = sorted(
|
|
61
|
+
({"start": row["start"], "end": row["end"]} for row in rows if row["text"].strip()),
|
|
62
|
+
key=lambda row: (row["start"], row["end"]),
|
|
63
|
+
)
|
|
114
64
|
merged = []
|
|
115
|
-
for span in
|
|
65
|
+
for span in spans:
|
|
116
66
|
if merged and span["start"] <= merged[-1]["end"] + 0.05:
|
|
117
67
|
merged[-1]["end"] = max(merged[-1]["end"], span["end"])
|
|
118
68
|
else:
|
|
119
|
-
merged.append(
|
|
69
|
+
merged.append(span)
|
|
120
70
|
return merged
|
|
121
71
|
|
|
122
72
|
|
|
123
73
|
def _combine_boundary_windows(*groups):
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
unique = {(round(row["start"], 3), round(row["end"], 3)) for row in rows}
|
|
74
|
+
unique = {
|
|
75
|
+
(round(row["start"], 3), round(row["end"], 3)) for group in groups for row in group
|
|
76
|
+
}
|
|
128
77
|
return [{"start": start, "end": end} for start, end in sorted(unique)]
|
|
129
78
|
|
|
130
79
|
|
|
131
80
|
def _same_source(left, right):
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
and right_id not in (None, "")
|
|
137
|
-
and str(left_id) == str(right_id)
|
|
138
|
-
)
|
|
139
|
-
left_path, right_path = left.get("source_path"), right.get("source_path")
|
|
140
|
-
if left_path or right_path:
|
|
141
|
-
return bool(left_path and right_path and str(left_path) == str(right_path))
|
|
142
|
-
return True # single-source normalized plans intentionally omit source identity
|
|
81
|
+
# Single-source normalized plans omit source identity; both sides then read as None.
|
|
82
|
+
return left.get("source_id") == right.get("source_id") and left.get(
|
|
83
|
+
"source_path"
|
|
84
|
+
) == right.get("source_path")
|
|
143
85
|
|
|
144
86
|
|
|
145
87
|
def _continuous_source_join(left, right, tolerance=0.05):
|
|
146
|
-
return
|
|
88
|
+
return (
|
|
147
89
|
_same_source(left, right)
|
|
148
|
-
and abs(
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
<= tolerance
|
|
152
|
-
and abs(
|
|
153
|
-
float(left.get("output_end", 0.0)) - float(right.get("output_start", 0.0))
|
|
154
|
-
)
|
|
155
|
-
<= tolerance
|
|
90
|
+
and abs(left["source_end"] - right["source_start"]) <= tolerance
|
|
91
|
+
and abs(left["output_end"] - right["output_start"]) <= tolerance
|
|
156
92
|
)
|
|
157
93
|
|
|
158
94
|
|
|
@@ -165,28 +101,14 @@ def enforce_clip_sentence_boundaries(
|
|
|
165
101
|
same-source join (no media is removed). Missing ASR timing degrades to `unchecked` rather
|
|
166
102
|
than inventing speech. Once ASR says an edge is speech-owned, failure to snap is blocking.
|
|
167
103
|
"""
|
|
168
|
-
clips = plan
|
|
169
|
-
windows = _valid_silence_windows(boundary_windows)
|
|
170
|
-
spans = _valid_silence_windows(speech_spans)
|
|
171
|
-
video_duration = max(0.0, float(video_duration or 0.0))
|
|
172
|
-
tolerance = max(0.001, float(tolerance or 0.05))
|
|
104
|
+
clips = plan["clips"]
|
|
173
105
|
checks, new_blockers = [], []
|
|
174
106
|
|
|
175
|
-
def
|
|
176
|
-
return any(
|
|
177
|
-
row["start"] - tolerance <= ts <= row["end"] + tolerance for row in windows
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
def in_speech(ts):
|
|
181
|
-
return any(
|
|
182
|
-
row["start"] - tolerance <= ts <= row["end"] + tolerance for row in spans
|
|
183
|
-
)
|
|
107
|
+
def inside(rows, ts):
|
|
108
|
+
return any(row["start"] - tolerance <= ts <= row["end"] + tolerance for row in rows)
|
|
184
109
|
|
|
185
110
|
for idx, clip in enumerate(clips):
|
|
186
|
-
for edge, ts in (
|
|
187
|
-
("start", float(clip["source_start"])),
|
|
188
|
-
("end", float(clip["source_end"])),
|
|
189
|
-
):
|
|
111
|
+
for edge, ts in (("start", clip["source_start"]), ("end", clip["source_end"])):
|
|
190
112
|
contiguous = (
|
|
191
113
|
edge == "start"
|
|
192
114
|
and idx > 0
|
|
@@ -202,16 +124,16 @@ def enforce_clip_sentence_boundaries(
|
|
|
202
124
|
status, reason = "safe", "source_end"
|
|
203
125
|
elif contiguous:
|
|
204
126
|
status, reason = "safe", "continuous_source_join"
|
|
205
|
-
elif
|
|
127
|
+
elif inside(boundary_windows, ts):
|
|
206
128
|
status, reason = "safe", "sentence_or_quiet_boundary"
|
|
207
|
-
elif not
|
|
129
|
+
elif not speech_spans:
|
|
208
130
|
status, reason = "unchecked", "speech_timing_unavailable"
|
|
209
|
-
elif not
|
|
131
|
+
elif not inside(speech_spans, ts):
|
|
210
132
|
status, reason = "safe", "outside_detected_speech"
|
|
211
133
|
else:
|
|
212
134
|
status, reason = "blocking", "inside_detected_speech"
|
|
213
135
|
check = {
|
|
214
|
-
"clip_id": clip
|
|
136
|
+
"clip_id": clip["clip_id"],
|
|
215
137
|
"source_id": clip.get("source_id"),
|
|
216
138
|
"edge": edge,
|
|
217
139
|
"time": round(ts, 3),
|
|
@@ -232,9 +154,8 @@ def enforce_clip_sentence_boundaries(
|
|
|
232
154
|
qc.setdefault("boundary_status", {})["sentence_checks"] = checks
|
|
233
155
|
existing = [
|
|
234
156
|
row
|
|
235
|
-
for row in
|
|
236
|
-
if
|
|
237
|
-
or row.get("code") != "unsafe_clip_sentence_boundary"
|
|
157
|
+
for row in qc.get("blocking", [])
|
|
158
|
+
if row["code"] != "unsafe_clip_sentence_boundary"
|
|
238
159
|
]
|
|
239
160
|
if existing or new_blockers:
|
|
240
161
|
qc["blocking"] = existing + new_blockers
|
|
@@ -246,7 +167,7 @@ def enforce_clip_sentence_boundaries(
|
|
|
246
167
|
def _recompute_clip_timeline(clips):
|
|
247
168
|
cursor = 0.0
|
|
248
169
|
for clip in clips:
|
|
249
|
-
duration = round(
|
|
170
|
+
duration = round(clip["source_end"] - clip["source_start"], 3)
|
|
250
171
|
clip["duration"] = duration
|
|
251
172
|
clip["output_start"] = round(cursor, 3)
|
|
252
173
|
clip["output_end"] = round(cursor + duration, 3)
|
|
@@ -254,15 +175,26 @@ def _recompute_clip_timeline(clips):
|
|
|
254
175
|
return round(cursor, 3)
|
|
255
176
|
|
|
256
177
|
|
|
178
|
+
def _plan_with_snapped_clips(plan, clips, boundary_key, events):
|
|
179
|
+
"""Copy of `plan` carrying the snapped clips, a cursor-based output timeline, and the
|
|
180
|
+
per-pass boundary events under qc.boundary_status[boundary_key]."""
|
|
181
|
+
result = dict(plan)
|
|
182
|
+
result["clips"] = clips
|
|
183
|
+
result["total_duration"] = _recompute_clip_timeline(clips)
|
|
184
|
+
qc = dict(plan.get("qc", {}))
|
|
185
|
+
boundary = dict(qc.get("boundary_status", {}))
|
|
186
|
+
boundary[boundary_key] = events
|
|
187
|
+
qc["boundary_status"] = boundary
|
|
188
|
+
result["qc"] = qc
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
257
192
|
def _candidate_overlaps(clips, idx, new_start, new_end):
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if
|
|
262
|
-
|
|
263
|
-
):
|
|
264
|
-
return True
|
|
265
|
-
return False
|
|
193
|
+
return any(
|
|
194
|
+
new_start < other["source_end"] and new_end > other["source_start"]
|
|
195
|
+
for j, other in enumerate(clips)
|
|
196
|
+
if j != idx
|
|
197
|
+
)
|
|
266
198
|
|
|
267
199
|
|
|
268
200
|
def snap_clip_starts_to_lines(
|
|
@@ -282,23 +214,18 @@ def snap_clip_starts_to_lines(
|
|
|
282
214
|
(<= max_trim) may trim forward if duration/overlap safety holds.
|
|
283
215
|
- never overlap/collapse when allow_overlap is false; unsafe attempts keep-and-warn.
|
|
284
216
|
"""
|
|
285
|
-
silence_periods = _valid_silence_windows(silence_periods)
|
|
286
217
|
if not silence_periods:
|
|
287
218
|
return plan
|
|
288
219
|
|
|
289
220
|
clips = [dict(c) for c in plan["clips"]]
|
|
290
|
-
|
|
291
|
-
max_prepend = max(0.0, float(max_prepend or 0.0))
|
|
292
|
-
max_trim = max(0.0, float(max_trim or 0.0))
|
|
293
|
-
min_duration = max(0.05, float(min_clip_duration or 0.05))
|
|
294
|
-
allow_overlap = bool(plan.get("allow_overlap", False))
|
|
221
|
+
allow_overlap = plan["allow_overlap"]
|
|
295
222
|
events = []
|
|
296
223
|
|
|
297
224
|
for i, clip in enumerate(clips):
|
|
298
|
-
original_start =
|
|
299
|
-
source_end =
|
|
225
|
+
original_start = clip["source_start"]
|
|
226
|
+
source_end = clip["source_end"]
|
|
300
227
|
event = {
|
|
301
|
-
"clip_id": clip
|
|
228
|
+
"clip_id": clip["clip_id"],
|
|
302
229
|
"source_id": clip.get("source_id"),
|
|
303
230
|
"original_start": round(original_start, 3),
|
|
304
231
|
"action": "kept",
|
|
@@ -313,12 +240,10 @@ def snap_clip_starts_to_lines(
|
|
|
313
240
|
if prior_ends:
|
|
314
241
|
candidate_start = max(prior_ends)
|
|
315
242
|
if original_start - candidate_start <= max_prepend:
|
|
316
|
-
candidate_start = round(
|
|
317
|
-
safe = candidate_start < source_end -
|
|
243
|
+
candidate_start = round(candidate_start, 3)
|
|
244
|
+
safe = candidate_start < source_end - min_clip_duration + 1e-9
|
|
318
245
|
if safe and not allow_overlap:
|
|
319
|
-
safe = not _candidate_overlaps(
|
|
320
|
-
clips, i, candidate_start, source_end
|
|
321
|
-
)
|
|
246
|
+
safe = not _candidate_overlaps(clips, i, candidate_start, source_end)
|
|
322
247
|
if safe:
|
|
323
248
|
clip["source_start"] = candidate_start
|
|
324
249
|
event.update(
|
|
@@ -336,19 +261,15 @@ def snap_clip_starts_to_lines(
|
|
|
336
261
|
else:
|
|
337
262
|
reason = "no_prior_quiet"
|
|
338
263
|
|
|
339
|
-
next_starts = [
|
|
340
|
-
w["start"] for w in silence_periods if w["start"] >= original_start
|
|
341
|
-
]
|
|
264
|
+
next_starts = [w["start"] for w in silence_periods if w["start"] >= original_start]
|
|
342
265
|
if next_starts:
|
|
343
266
|
candidate_start = min(next_starts)
|
|
344
267
|
trim_delta = candidate_start - original_start
|
|
345
268
|
if 0 < trim_delta <= max_trim:
|
|
346
269
|
candidate_start = round(min(video_duration, candidate_start), 3)
|
|
347
|
-
safe = source_end - candidate_start >=
|
|
270
|
+
safe = source_end - candidate_start >= min_clip_duration
|
|
348
271
|
if safe and not allow_overlap:
|
|
349
|
-
safe = not _candidate_overlaps(
|
|
350
|
-
clips, i, candidate_start, source_end
|
|
351
|
-
)
|
|
272
|
+
safe = not _candidate_overlaps(clips, i, candidate_start, source_end)
|
|
352
273
|
if safe:
|
|
353
274
|
clip["source_start"] = candidate_start
|
|
354
275
|
event.update(
|
|
@@ -367,69 +288,53 @@ def snap_clip_starts_to_lines(
|
|
|
367
288
|
event["warning_code"] = "clip_start_unsnapped"
|
|
368
289
|
events.append(event)
|
|
369
290
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
result["clips"] = clips
|
|
373
|
-
result["total_duration"] = total_duration
|
|
374
|
-
qc = dict(result.get("qc") or {})
|
|
375
|
-
boundary = dict(qc.get("boundary_status") or {})
|
|
376
|
-
boundary["start_snaps"] = events
|
|
377
|
-
qc["boundary_status"] = boundary
|
|
378
|
-
warnings = list(qc.get("warnings") or [])
|
|
291
|
+
result = _plan_with_snapped_clips(plan, clips, "start_snaps", events)
|
|
292
|
+
warnings = list(result["qc"].get("warnings", []))
|
|
379
293
|
for event in events:
|
|
380
|
-
if
|
|
294
|
+
if "warning_code" in event:
|
|
381
295
|
warnings.append(
|
|
382
296
|
{
|
|
383
297
|
"code": event["warning_code"],
|
|
384
298
|
"clip_id": event["clip_id"],
|
|
385
|
-
"source_id": event
|
|
386
|
-
"start_unsnapped_reason": event
|
|
299
|
+
"source_id": event["source_id"],
|
|
300
|
+
"start_unsnapped_reason": event["start_unsnapped_reason"],
|
|
387
301
|
}
|
|
388
302
|
)
|
|
389
303
|
if warnings:
|
|
390
|
-
qc["warnings"] = warnings
|
|
391
|
-
result["qc"] = qc
|
|
304
|
+
result["qc"]["warnings"] = warnings
|
|
392
305
|
return result
|
|
393
306
|
|
|
394
307
|
|
|
395
308
|
def snap_clip_ends_to_lines(plan, silence_periods, video_duration, max_extend):
|
|
396
309
|
"""Extend each clip's source_end forward to the next natural pause, preventing mid-sentence cuts.
|
|
397
310
|
|
|
398
|
-
-
|
|
399
|
-
-
|
|
400
|
-
- Otherwise
|
|
401
|
-
- When plan["allow_overlap"] is False,
|
|
402
|
-
-
|
|
403
|
-
- Returns the updated plan dict (all other keys preserved).
|
|
311
|
+
- No quiet windows: the plan is returned unchanged.
|
|
312
|
+
- A source_end already inside a quiet window is left alone.
|
|
313
|
+
- Otherwise extend to the next quiet window start, capped by max_extend and video_duration.
|
|
314
|
+
- When plan["allow_overlap"] is False, never extend into another clip's source range.
|
|
315
|
+
- Recomputes output_start/output_end/duration for all clips cursor-based.
|
|
404
316
|
"""
|
|
405
|
-
# silence_periods is loaded from disk; tolerate a stale/hand-edited row by keeping only
|
|
406
|
-
# well-formed quiet windows so a single bad entry can't KeyError-abort the whole cut.
|
|
407
|
-
silence_periods = _valid_silence_windows(silence_periods)
|
|
408
317
|
if not silence_periods:
|
|
409
318
|
return plan
|
|
410
319
|
|
|
411
320
|
clips = [dict(c) for c in plan["clips"]]
|
|
412
|
-
|
|
413
|
-
max_extend = float(max_extend)
|
|
414
|
-
allow_overlap = bool(plan.get("allow_overlap", False))
|
|
321
|
+
allow_overlap = plan["allow_overlap"]
|
|
415
322
|
events = []
|
|
416
323
|
|
|
417
324
|
for i, clip in enumerate(clips):
|
|
418
325
|
source_end = clip["source_end"]
|
|
419
326
|
event = {
|
|
420
|
-
"clip_id": clip
|
|
327
|
+
"clip_id": clip["clip_id"],
|
|
421
328
|
"source_id": clip.get("source_id"),
|
|
422
|
-
"original_end": round(
|
|
329
|
+
"original_end": round(source_end, 3),
|
|
423
330
|
"action": "kept",
|
|
424
331
|
}
|
|
425
332
|
|
|
426
|
-
# Already inside a quiet window → already at a natural pause.
|
|
427
333
|
if any(w["start"] <= source_end <= w["end"] for w in silence_periods):
|
|
428
334
|
event["reason"] = "already_quiet"
|
|
429
335
|
events.append(event)
|
|
430
336
|
continue
|
|
431
337
|
|
|
432
|
-
# Find the next quiet window start at or after source_end.
|
|
433
338
|
candidates = [w["start"] for w in silence_periods if w["start"] >= source_end]
|
|
434
339
|
if not candidates:
|
|
435
340
|
event["end_unsnapped_reason"] = "no_next_quiet"
|
|
@@ -437,7 +342,6 @@ def snap_clip_ends_to_lines(plan, silence_periods, video_duration, max_extend):
|
|
|
437
342
|
continue
|
|
438
343
|
next_quiet_start = min(candidates)
|
|
439
344
|
|
|
440
|
-
# Only snap if the next pause is within reach (≤ max_extend away).
|
|
441
345
|
if next_quiet_start > source_end + max_extend:
|
|
442
346
|
event["end_unsnapped_reason"] = "next_quiet_too_far"
|
|
443
347
|
events.append(event)
|
|
@@ -456,8 +360,7 @@ def snap_clip_ends_to_lines(plan, silence_periods, video_duration, max_extend):
|
|
|
456
360
|
if j != i and c["source_start"] > source_end
|
|
457
361
|
]
|
|
458
362
|
if other_starts:
|
|
459
|
-
|
|
460
|
-
candidate_end = min(candidate_end, nearest_other_start)
|
|
363
|
+
candidate_end = min(candidate_end, min(other_starts))
|
|
461
364
|
if candidate_end <= source_end:
|
|
462
365
|
event["end_unsnapped_reason"] = "overlap_or_collapse"
|
|
463
366
|
events.append(event)
|
|
@@ -468,23 +371,12 @@ def snap_clip_ends_to_lines(plan, silence_periods, video_duration, max_extend):
|
|
|
468
371
|
{
|
|
469
372
|
"action": "extended",
|
|
470
373
|
"new_end": clip["source_end"],
|
|
471
|
-
"delta": round(
|
|
374
|
+
"delta": round(clip["source_end"] - source_end, 3),
|
|
472
375
|
}
|
|
473
376
|
)
|
|
474
377
|
events.append(event)
|
|
475
378
|
|
|
476
|
-
|
|
477
|
-
total_duration = _recompute_clip_timeline(clips)
|
|
478
|
-
|
|
479
|
-
result = dict(plan)
|
|
480
|
-
result["clips"] = clips
|
|
481
|
-
result["total_duration"] = total_duration
|
|
482
|
-
qc = dict(result.get("qc") or {})
|
|
483
|
-
boundary = dict(qc.get("boundary_status") or {})
|
|
484
|
-
boundary["end_snaps"] = events
|
|
485
|
-
qc["boundary_status"] = boundary
|
|
486
|
-
result["qc"] = qc
|
|
487
|
-
return result
|
|
379
|
+
return _plan_with_snapped_clips(plan, clips, "end_snaps", events)
|
|
488
380
|
|
|
489
381
|
|
|
490
382
|
def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
|
|
@@ -493,10 +385,8 @@ def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
|
|
|
493
385
|
Input-seek to a little before the window: the rebased output PTS restarts at ~0 at the seek
|
|
494
386
|
target, so `seek + pts_time` recovers absolute source time. The `lead` keeps the seek/keyframe
|
|
495
387
|
settling artifact frame outside [win_start, win_end] so it is filtered out, not mistaken for a
|
|
496
|
-
cut.
|
|
388
|
+
cut.
|
|
497
389
|
"""
|
|
498
|
-
win_start = max(0.0, float(win_start))
|
|
499
|
-
win_end = max(win_start, float(win_end))
|
|
500
390
|
if win_end - win_start < 1e-3:
|
|
501
391
|
return []
|
|
502
392
|
seek = max(0.0, win_start - lead)
|
|
@@ -519,21 +409,18 @@ def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
|
|
|
519
409
|
"null",
|
|
520
410
|
"-",
|
|
521
411
|
]
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
for m in re.finditer(r"pts_time:([0-9.]+)", proc.stderr or ""):
|
|
412
|
+
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
413
|
+
if proc.returncode != 0:
|
|
414
|
+
raise RuntimeError(f"ffmpeg 切镜头检测失败: {video}: {proc.stderr.strip()[-500:]}")
|
|
415
|
+
changes = set()
|
|
416
|
+
for m in re.finditer(r"pts_time:([0-9.]+)", proc.stderr):
|
|
528
417
|
t = seek + float(m.group(1))
|
|
529
418
|
if win_start <= t <= win_end:
|
|
530
|
-
changes.
|
|
531
|
-
return sorted(
|
|
419
|
+
changes.add(round(t, 3))
|
|
420
|
+
return sorted(changes)
|
|
532
421
|
|
|
533
422
|
|
|
534
|
-
def snap_clips_off_shot_changes(
|
|
535
|
-
plan, video, video_duration, margin, threshold, min_keep=0.5
|
|
536
|
-
):
|
|
423
|
+
def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
|
|
537
424
|
"""Nudge each clip's boundaries clear of the ORIGINAL footage's hard cuts to avoid 闪烁.
|
|
538
425
|
|
|
539
426
|
A clip whose source_start sits just before a shot-change opens on a brief sliver of the old
|
|
@@ -542,22 +429,19 @@ def snap_clips_off_shot_changes(
|
|
|
542
429
|
- move source_start FORWARD onto a shot-change in (start, start+margin] → clean open
|
|
543
430
|
- move source_end BACK onto a shot-change in [end-margin, end) → clean close
|
|
544
431
|
Boundaries already on a cut, or with no nearby cut, are left untouched. Snaps that would shrink
|
|
545
|
-
a clip below `min_keep` are skipped. Recomputes the output timeline cursor-based.
|
|
546
|
-
detection failure leaves that boundary as-is.
|
|
432
|
+
a clip below `min_keep` are skipped. Recomputes the output timeline cursor-based.
|
|
547
433
|
"""
|
|
548
|
-
|
|
549
|
-
margin = float(margin)
|
|
550
|
-
if margin <= 0 or not plan.get("clips"):
|
|
434
|
+
if margin <= 0:
|
|
551
435
|
return plan
|
|
552
436
|
clips = [dict(c) for c in plan["clips"]]
|
|
553
437
|
n_start = n_end = 0
|
|
554
438
|
events = []
|
|
555
|
-
for
|
|
556
|
-
s =
|
|
557
|
-
e =
|
|
439
|
+
for clip in clips:
|
|
440
|
+
s = clip["source_start"]
|
|
441
|
+
e = clip["source_end"]
|
|
558
442
|
new_s, new_e = s, e
|
|
559
443
|
event = {
|
|
560
|
-
"clip_id": clip
|
|
444
|
+
"clip_id": clip["clip_id"],
|
|
561
445
|
"source_id": clip.get("source_id"),
|
|
562
446
|
"original_start": round(s, 3),
|
|
563
447
|
"original_end": round(e, 3),
|
|
@@ -598,36 +482,17 @@ def snap_clips_off_shot_changes(
|
|
|
598
482
|
clip["source_end"] = new_e
|
|
599
483
|
events.append(event)
|
|
600
484
|
|
|
601
|
-
# Recompute output timeline cursor-based (same cursor logic as snap_clip_ends_to_lines).
|
|
602
|
-
total_duration = _recompute_clip_timeline(clips)
|
|
603
|
-
|
|
604
485
|
if n_start or n_end:
|
|
605
486
|
log(
|
|
606
487
|
f"避让原片切镜头: {n_start} 个起点前移、{n_end} 个终点回收 (margin={margin}s, 阈值={threshold})"
|
|
607
488
|
)
|
|
608
|
-
|
|
609
|
-
result["clips"] = clips
|
|
610
|
-
result["total_duration"] = total_duration
|
|
611
|
-
qc = dict(result.get("qc") or {})
|
|
612
|
-
boundary = dict(qc.get("boundary_status") or {})
|
|
613
|
-
boundary["shot_snaps"] = events
|
|
614
|
-
qc["boundary_status"] = boundary
|
|
615
|
-
result["qc"] = qc
|
|
616
|
-
return result
|
|
489
|
+
return _plan_with_snapped_clips(plan, clips, "shot_snaps", events)
|
|
617
490
|
|
|
618
491
|
|
|
619
492
|
def _load_silence_for_source(work_dir, source_id, source_work_dir=None):
|
|
620
|
-
"""Read a source's silence_periods.json
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
):
|
|
624
|
-
if path.exists():
|
|
625
|
-
try:
|
|
626
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
627
|
-
except (OSError, ValueError):
|
|
628
|
-
return []
|
|
629
|
-
return data if isinstance(data, list) else []
|
|
630
|
-
return []
|
|
493
|
+
"""Read a source's silence_periods.json (project layout when source_id is None); [] when absent."""
|
|
494
|
+
path = _find_source_artifact(work_dir, "silence_periods.json", source_id, source_work_dir)
|
|
495
|
+
return json.loads(path.read_text(encoding="utf-8")) if path else []
|
|
631
496
|
|
|
632
497
|
|
|
633
498
|
def snap_multi_source_clips(
|
|
@@ -638,25 +503,23 @@ def snap_multi_source_clips(
|
|
|
638
503
|
line_max_extend,
|
|
639
504
|
scene_margin,
|
|
640
505
|
scene_threshold,
|
|
506
|
+
start_max_prepend,
|
|
507
|
+
start_max_trim,
|
|
641
508
|
do_line_snap=True,
|
|
642
509
|
do_scene_snap=True,
|
|
643
|
-
start_max_prepend=None,
|
|
644
|
-
start_max_trim=None,
|
|
645
510
|
):
|
|
646
511
|
"""Per-source line/shot snapping for a multi-source validated plan.
|
|
647
512
|
|
|
648
513
|
Each clip is snapped using ITS OWN source's silence windows / shot changes and duration
|
|
649
514
|
(a clip in source B never constrains a clip in source A), then the global OUTPUT timeline
|
|
650
|
-
is recomputed once in plan order.
|
|
651
|
-
|
|
515
|
+
is recomputed once in plan order. Missing silence data leaves a boundary unchanged.
|
|
516
|
+
Mirrors the single-source snap_clip_ends_to_lines + snap_clips_off_shot_changes.
|
|
652
517
|
"""
|
|
653
|
-
clips = plan
|
|
654
|
-
|
|
655
|
-
return plan
|
|
656
|
-
allow_overlap = bool(plan.get("allow_overlap", False))
|
|
518
|
+
clips = plan["clips"]
|
|
519
|
+
allow_overlap = plan["allow_overlap"]
|
|
657
520
|
groups = {}
|
|
658
521
|
for clip in clips:
|
|
659
|
-
groups.setdefault(
|
|
522
|
+
groups.setdefault(clip["source_id"], []).append(clip)
|
|
660
523
|
boundary_accum = {
|
|
661
524
|
"start_snaps": [],
|
|
662
525
|
"end_snaps": [],
|
|
@@ -665,34 +528,24 @@ def snap_multi_source_clips(
|
|
|
665
528
|
}
|
|
666
529
|
blocking_accum = []
|
|
667
530
|
for sid, group in groups.items():
|
|
668
|
-
source = sources
|
|
669
|
-
duration =
|
|
670
|
-
(float(c["source_end"]) for c in group), default=0.0
|
|
671
|
-
)
|
|
531
|
+
source = sources[sid]
|
|
532
|
+
duration = source["duration"]
|
|
672
533
|
mini = {"clips": [dict(c) for c in group], "allow_overlap": allow_overlap}
|
|
673
534
|
# Visual cleanup goes first. Sentence/quiet snapping is the final authority because
|
|
674
535
|
# a clean picture is never allowed to reintroduce a mid-sentence audio cut.
|
|
675
|
-
if do_scene_snap
|
|
536
|
+
if do_scene_snap:
|
|
676
537
|
mini = snap_clips_off_shot_changes(
|
|
677
|
-
mini,
|
|
678
|
-
source["source_path"],
|
|
679
|
-
video_duration=duration,
|
|
680
|
-
margin=scene_margin,
|
|
681
|
-
threshold=scene_threshold,
|
|
538
|
+
mini, source["source_path"], margin=scene_margin, threshold=scene_threshold
|
|
682
539
|
)
|
|
683
540
|
source_work_dir = source.get("source_work_dir")
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
541
|
+
boundaries = _combine_boundary_windows(
|
|
542
|
+
_load_silence_for_source(work_dir, sid, source_work_dir),
|
|
543
|
+
_load_sentence_boundary_windows(work_dir, sid, source_work_dir),
|
|
544
|
+
)
|
|
687
545
|
if do_line_snap:
|
|
688
|
-
|
|
689
|
-
mini =
|
|
690
|
-
|
|
691
|
-
boundaries,
|
|
692
|
-
duration,
|
|
693
|
-
start_max_prepend,
|
|
694
|
-
max_trim=start_max_trim if start_max_trim is not None else 0.35,
|
|
695
|
-
)
|
|
546
|
+
mini = snap_clip_starts_to_lines(
|
|
547
|
+
mini, boundaries, duration, start_max_prepend, max_trim=start_max_trim
|
|
548
|
+
)
|
|
696
549
|
mini = snap_clip_ends_to_lines(mini, boundaries, duration, line_max_extend)
|
|
697
550
|
mini = enforce_clip_sentence_boundaries(
|
|
698
551
|
mini,
|
|
@@ -700,46 +553,33 @@ def snap_multi_source_clips(
|
|
|
700
553
|
_load_source_speech_spans(work_dir, sid, source_work_dir),
|
|
701
554
|
duration,
|
|
702
555
|
)
|
|
703
|
-
mini_boundary =
|
|
704
|
-
for key in boundary_accum:
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
)
|
|
708
|
-
blocking_accum.extend(
|
|
709
|
-
[
|
|
710
|
-
row
|
|
711
|
-
for row in ((mini.get("qc") or {}).get("blocking") or [])
|
|
712
|
-
if isinstance(row, dict)
|
|
713
|
-
]
|
|
714
|
-
)
|
|
556
|
+
mini_boundary = mini["qc"]["boundary_status"]
|
|
557
|
+
for key, events in boundary_accum.items():
|
|
558
|
+
events.extend(mini_boundary.get(key, []))
|
|
559
|
+
blocking_accum.extend(mini["qc"].get("blocking", []))
|
|
715
560
|
for original, snapped in zip(group, mini["clips"]):
|
|
716
561
|
original["source_start"] = snapped["source_start"]
|
|
717
562
|
original["source_end"] = snapped["source_end"]
|
|
718
563
|
# Recompute the global output timeline cursor-based, in plan order (not group order).
|
|
719
564
|
plan["total_duration"] = _recompute_clip_timeline(clips)
|
|
720
565
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
)
|
|
739
|
-
# Blocking rows are propagated unconditionally. They used to sit inside the
|
|
740
|
-
# `any(boundary_accum.values())` branch, which happens to hold today only because
|
|
741
|
-
# enforce_clip_sentence_boundaries always emits sentence_checks — a mid-sentence cut
|
|
742
|
-
# must never be silently dropped because some sibling QC list came back empty.
|
|
566
|
+
qc = plan.setdefault("qc", {})
|
|
567
|
+
boundary = qc.setdefault("boundary_status", {})
|
|
568
|
+
for key, events in boundary_accum.items():
|
|
569
|
+
if events:
|
|
570
|
+
boundary.setdefault(key, []).extend(events)
|
|
571
|
+
warnings = qc.setdefault("warnings", [])
|
|
572
|
+
for event in boundary_accum["start_snaps"]:
|
|
573
|
+
if "warning_code" in event:
|
|
574
|
+
warnings.append(
|
|
575
|
+
{
|
|
576
|
+
"code": event["warning_code"],
|
|
577
|
+
"clip_id": event["clip_id"],
|
|
578
|
+
"source_id": event["source_id"],
|
|
579
|
+
"start_unsnapped_reason": event["start_unsnapped_reason"],
|
|
580
|
+
}
|
|
581
|
+
)
|
|
582
|
+
# A mid-sentence cut must never be dropped because some sibling QC list came back empty.
|
|
743
583
|
if blocking_accum:
|
|
744
|
-
|
|
584
|
+
qc.setdefault("blocking", []).extend(blocking_accum)
|
|
745
585
|
return plan
|