@oh-story/dsh 0.1.11 → 0.1.13
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 +17 -11
- package/lib/client.js +85 -35
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +41 -36
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
- package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
- package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
- 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 +5 -0
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
- package/lib/index.js +16 -7
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/README.md +98 -44
- package/lib/novel-to-game/README_ZH.md +100 -46
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
- package/lib/novel-to-game/manifest.json +303 -78
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
- package/lib/types/client/drama-production-view.d.ts +11 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +1 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +7 -2
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.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/video-recap/CHANGELOG.md +108 -1
- package/lib/video-recap/README.en.md +18 -14
- package/lib/video-recap/README.md +18 -14
- package/lib/video-recap/manifest.json +297 -321
- package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
- package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
- package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
- package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
- package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
- package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
- package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
- package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
- package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
- package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
- package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
- package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
- package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
- package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
- package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
- package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
- package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
- package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
- package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
- package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
- package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
- package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
- package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
- package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
- package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
- package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
- package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
- package/lib/video-recap/skills/video-script/SKILL.md +9 -7
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
- package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
- package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
- package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
- package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
- package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
- package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
- package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
- package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
- package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
- package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
- package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
- package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
- package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
- package/package.json +45 -45
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
- /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
|
@@ -5,7 +5,16 @@ import re
|
|
|
5
5
|
import subprocess
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from frame_grid import edge_frame_snapper, snap_edges_to_frames
|
|
8
9
|
from lib import log
|
|
10
|
+
from sentence_gate import (
|
|
11
|
+
_GATE_TOLERANCE,
|
|
12
|
+
_continuous_source_join,
|
|
13
|
+
_edge_classifier,
|
|
14
|
+
_edge_joined,
|
|
15
|
+
_unverified_window,
|
|
16
|
+
enforce_clip_sentence_boundaries,
|
|
17
|
+
)
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
def _find_source_artifact(work_dir, filename, source_id=None, source_work_dir=None):
|
|
@@ -20,11 +29,12 @@ def _find_source_artifact(work_dir, filename, source_id=None, source_work_dir=No
|
|
|
20
29
|
|
|
21
30
|
|
|
22
31
|
def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=None):
|
|
23
|
-
"""Load
|
|
32
|
+
"""Load usable sentence-end pause windows produced by video-understanding.
|
|
24
33
|
|
|
25
34
|
A sentence anchor's `time` is the acoustic pause end, while `pause_start` is already
|
|
26
|
-
after the final spoken sample. Any cut within that closed interval
|
|
27
|
-
|
|
35
|
+
after the final spoken sample. Any cut within that closed interval is word-safe.
|
|
36
|
+
`boundary_use: unverified` anchors (coarse-ASR estimates) stay usable but keep that
|
|
37
|
+
label so an edge resting on one is reported as such; `none` anchors are excluded.
|
|
28
38
|
"""
|
|
29
39
|
path = _find_source_artifact(
|
|
30
40
|
work_dir, "speech_boundary_anchors.json", source_id, source_work_dir
|
|
@@ -32,35 +42,80 @@ def _load_sentence_boundary_windows(work_dir, source_id=None, source_work_dir=No
|
|
|
32
42
|
if path is None:
|
|
33
43
|
return []
|
|
34
44
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
35
|
-
windows = [
|
|
36
|
-
|
|
45
|
+
windows = []
|
|
46
|
+
for anchor in payload["sentence_anchors"]:
|
|
47
|
+
# Schema-1 anchors (no `boundary_use`) came from the old coarse estimator: high/medium
|
|
48
|
+
# labels there are usable but unverified.
|
|
49
|
+
use = anchor.get("boundary_use") or (
|
|
50
|
+
"unverified" if anchor["confidence"] in {"high", "medium"} else "none"
|
|
51
|
+
)
|
|
52
|
+
if use == "none":
|
|
53
|
+
continue
|
|
54
|
+
windows.append({
|
|
37
55
|
"start": round(anchor["pause_start"], 3),
|
|
38
56
|
"end": round(anchor["time"], 3),
|
|
39
57
|
"kind": "sentence_anchor",
|
|
40
58
|
"confidence": anchor["confidence"],
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if anchor["confidence"] in {"high", "medium"}
|
|
44
|
-
]
|
|
59
|
+
"boundary_use": use,
|
|
60
|
+
})
|
|
45
61
|
return sorted(windows, key=lambda row: (row["start"], row["end"]))
|
|
46
62
|
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
# Interjections and common ASR artifacts on screams/music. A window whose text is only these
|
|
65
|
+
# is not dialogue at a clip edge or narration entry; real short lines such as "救我!" still are.
|
|
66
|
+
# Same copy in video-cut, video-script and video-assemble (parity-tested by function).
|
|
67
|
+
_NON_DIALOGUE_TOKENS = frozenset(
|
|
68
|
+
"啊 嗯 哼 哦 呃 唉 嘿 呦 哈 呀 hi yeah ok okay oh uh ah hmm".split()
|
|
69
|
+
)
|
|
70
|
+
_NON_DIALOGUE_CJK = frozenset("啊嗯哼哦呃唉嘿呦哈呀")
|
|
71
|
+
# Lines cross 15s ASR window edges (a line may run 13.2–15.4 while its window ends at 15.0),
|
|
72
|
+
# so an interjection-only window next to real dialogue keeps this much of its shared edge.
|
|
73
|
+
_INTERJECTION_GUARD_SECONDS = 1.0
|
|
50
74
|
|
|
51
|
-
|
|
75
|
+
|
|
76
|
+
def _interjection_only(text):
|
|
77
|
+
tokens = [token for token in re.split(r"[\W_]+", text.lower()) if token]
|
|
78
|
+
# Punctuation-only rows ("……", "?") are often ASR for unintelligible speech: keep them.
|
|
79
|
+
return bool(tokens) and all(
|
|
80
|
+
token in _NON_DIALOGUE_TOKENS or set(token) <= _NON_DIALOGUE_CJK for token in tokens
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _dialogue_speech_spans(rows):
|
|
85
|
+
"""Merged dialogue spans from timed ASR rows.
|
|
86
|
+
|
|
87
|
+
A row holding only interjections ("啊!", "Hi.") is not dialogue, except a
|
|
88
|
+
`_INTERJECTION_GUARD_SECONDS` guard on an edge it shares with a dialogue row. A row
|
|
89
|
+
whose text is empty or whitespace (ASR heard no words) is timing-only evidence: it is
|
|
90
|
+
skipped here and guards nothing. A row with no `text` field is measured timing whose
|
|
91
|
+
words are unknown and counts as dialogue.
|
|
52
92
|
"""
|
|
53
|
-
rows =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
rows = sorted(
|
|
94
|
+
(
|
|
95
|
+
{
|
|
96
|
+
"start": row["start"],
|
|
97
|
+
"end": row["end"],
|
|
98
|
+
"dialogue": not _interjection_only(row.get("text", "")),
|
|
99
|
+
}
|
|
100
|
+
for row in rows
|
|
101
|
+
if "text" not in row or row["text"].strip()
|
|
102
|
+
),
|
|
62
103
|
key=lambda row: (row["start"], row["end"]),
|
|
63
104
|
)
|
|
105
|
+
spans = []
|
|
106
|
+
for idx, row in enumerate(rows):
|
|
107
|
+
if row["dialogue"]:
|
|
108
|
+
spans.append({"start": row["start"], "end": row["end"]})
|
|
109
|
+
continue
|
|
110
|
+
before = rows[idx - 1] if idx > 0 else None
|
|
111
|
+
after = rows[idx + 1] if idx + 1 < len(rows) else None
|
|
112
|
+
if before and before["dialogue"] and row["start"] - before["end"] <= 0.05:
|
|
113
|
+
end = min(row["end"], row["start"] + _INTERJECTION_GUARD_SECONDS)
|
|
114
|
+
spans.append({"start": row["start"], "end": end})
|
|
115
|
+
if after and after["dialogue"] and after["start"] - row["end"] <= 0.05:
|
|
116
|
+
start = max(row["start"], row["end"] - _INTERJECTION_GUARD_SECONDS)
|
|
117
|
+
spans.append({"start": start, "end": row["end"]})
|
|
118
|
+
spans.sort(key=lambda row: (row["start"], row["end"]))
|
|
64
119
|
merged = []
|
|
65
120
|
for span in spans:
|
|
66
121
|
if merged and span["start"] <= merged[-1]["end"] + 0.05:
|
|
@@ -70,98 +125,36 @@ def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
|
|
|
70
125
|
return merged
|
|
71
126
|
|
|
72
127
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
(round(row["start"], 3), round(row["end"], 3)) for group in groups for row in group
|
|
76
|
-
}
|
|
77
|
-
return [{"start": start, "end": end} for start, end in sorted(unique)]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _same_source(left, right):
|
|
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")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _continuous_source_join(left, right, tolerance=0.05):
|
|
88
|
-
return (
|
|
89
|
-
_same_source(left, right)
|
|
90
|
-
and abs(left["source_end"] - right["source_start"]) <= tolerance
|
|
91
|
-
and abs(left["output_end"] - right["output_start"]) <= tolerance
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def enforce_clip_sentence_boundaries(
|
|
96
|
-
plan, boundary_windows, speech_spans, video_duration, tolerance=0.05
|
|
97
|
-
):
|
|
98
|
-
"""Block any audible clip edge that falls inside detected source speech.
|
|
128
|
+
def _load_source_speech_spans(work_dir, source_id=None, source_work_dir=None):
|
|
129
|
+
"""Merged ASR dialogue spans (asr_clean.json wins over asr_result.json).
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
same-source join (no media is removed). Missing ASR timing degrades to `unchecked` rather
|
|
102
|
-
than inventing speech. Once ASR says an edge is speech-owned, failure to snap is blocking.
|
|
131
|
+
Only used to decide whether an unsafe edge blocks; a missing transcript means unchecked.
|
|
103
132
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
contiguous = (
|
|
113
|
-
edge == "start"
|
|
114
|
-
and idx > 0
|
|
115
|
-
and _continuous_source_join(clips[idx - 1], clip, tolerance)
|
|
116
|
-
) or (
|
|
117
|
-
edge == "end"
|
|
118
|
-
and idx + 1 < len(clips)
|
|
119
|
-
and _continuous_source_join(clip, clips[idx + 1], tolerance)
|
|
120
|
-
)
|
|
121
|
-
if edge == "start" and ts <= tolerance:
|
|
122
|
-
status, reason = "safe", "source_start"
|
|
123
|
-
elif edge == "end" and ts >= video_duration - tolerance:
|
|
124
|
-
status, reason = "safe", "source_end"
|
|
125
|
-
elif contiguous:
|
|
126
|
-
status, reason = "safe", "continuous_source_join"
|
|
127
|
-
elif inside(boundary_windows, ts):
|
|
128
|
-
status, reason = "safe", "sentence_or_quiet_boundary"
|
|
129
|
-
elif not speech_spans:
|
|
130
|
-
status, reason = "unchecked", "speech_timing_unavailable"
|
|
131
|
-
elif not inside(speech_spans, ts):
|
|
132
|
-
status, reason = "safe", "outside_detected_speech"
|
|
133
|
-
else:
|
|
134
|
-
status, reason = "blocking", "inside_detected_speech"
|
|
135
|
-
check = {
|
|
136
|
-
"clip_id": clip["clip_id"],
|
|
137
|
-
"source_id": clip.get("source_id"),
|
|
138
|
-
"edge": edge,
|
|
139
|
-
"time": round(ts, 3),
|
|
140
|
-
"status": status,
|
|
141
|
-
"reason": reason,
|
|
142
|
-
}
|
|
143
|
-
checks.append(check)
|
|
144
|
-
if status == "blocking":
|
|
145
|
-
new_blockers.append(
|
|
146
|
-
{
|
|
147
|
-
"code": "unsafe_clip_sentence_boundary",
|
|
148
|
-
**check,
|
|
149
|
-
"message": "剪辑边界仍落在原声讲话区间内,必须移动到句末锚点,不能截断原声句子。",
|
|
150
|
-
}
|
|
151
|
-
)
|
|
133
|
+
rows = []
|
|
134
|
+
for filename in ("asr_clean.json", "asr_result.json"):
|
|
135
|
+
path = _find_source_artifact(work_dir, filename, source_id, source_work_dir)
|
|
136
|
+
if path is not None:
|
|
137
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
138
|
+
rows = payload["segments"] if filename == "asr_clean.json" else payload
|
|
139
|
+
break
|
|
140
|
+
return _dialogue_speech_spans(row for row in rows if row["text"].strip())
|
|
152
141
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
142
|
+
|
|
143
|
+
def _combine_boundary_windows(*groups):
|
|
144
|
+
"""Dedupe windows by span; a span backed by any verified/quiet row counts as verified."""
|
|
145
|
+
unique = {}
|
|
146
|
+
for group in groups:
|
|
147
|
+
for row in group:
|
|
148
|
+
key = (round(row["start"], 3), round(row["end"], 3))
|
|
149
|
+
if key not in unique or _unverified_window(unique[key]):
|
|
150
|
+
unique[key] = row
|
|
151
|
+
combined = []
|
|
152
|
+
for (start, end), row in sorted(unique.items()):
|
|
153
|
+
item = {"start": start, "end": end, "kind": row.get("kind", "quiet_window")}
|
|
154
|
+
if _unverified_window(row):
|
|
155
|
+
item["boundary_use"] = "unverified"
|
|
156
|
+
combined.append(item)
|
|
157
|
+
return combined
|
|
165
158
|
|
|
166
159
|
|
|
167
160
|
def _recompute_clip_timeline(clips):
|
|
@@ -420,7 +413,12 @@ def _detect_shot_changes(video, win_start, win_end, threshold, lead=0.25):
|
|
|
420
413
|
return sorted(changes)
|
|
421
414
|
|
|
422
415
|
|
|
423
|
-
|
|
416
|
+
# How far past each edge's own search window the shot-change scan also looks, so a hard cut
|
|
417
|
+
# within a frame on the other side of the edge is known to the frame-grid pass.
|
|
418
|
+
_FRAME_CUT_SCAN_PAD = 0.1
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5, found_cuts=None):
|
|
424
422
|
"""Nudge each clip's boundaries clear of the ORIGINAL footage's hard cuts to avoid 闪烁.
|
|
425
423
|
|
|
426
424
|
A clip whose source_start sits just before a shot-change opens on a brief sliver of the old
|
|
@@ -430,9 +428,14 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
|
|
|
430
428
|
- move source_end BACK onto a shot-change in [end-margin, end) → clean close
|
|
431
429
|
Boundaries already on a cut, or with no nearby cut, are left untouched. Snaps that would shrink
|
|
432
430
|
a clip below `min_keep` are skipped. Recomputes the output timeline cursor-based.
|
|
431
|
+
|
|
432
|
+
`found_cuts` (a set) collects every cut the scan saw, including those up to
|
|
433
|
+
`_FRAME_CUT_SCAN_PAD` on the far side of each edge, for the frame-grid pass's tie-break.
|
|
433
434
|
"""
|
|
434
435
|
if margin <= 0:
|
|
435
436
|
return plan
|
|
437
|
+
pad = _FRAME_CUT_SCAN_PAD if found_cuts is not None else 0.0
|
|
438
|
+
found_cuts = set() if found_cuts is None else found_cuts
|
|
436
439
|
clips = [dict(c) for c in plan["clips"]]
|
|
437
440
|
n_start = n_end = 0
|
|
438
441
|
events = []
|
|
@@ -449,11 +452,9 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
|
|
|
449
452
|
"end_action": "kept",
|
|
450
453
|
}
|
|
451
454
|
# Opening: a shot-change just AFTER source_start leaves an old-shot sliver before it.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if c > s + 1e-3
|
|
456
|
-
]
|
|
455
|
+
seen = _detect_shot_changes(video, max(0.0, s - pad), min(e, s + margin), threshold)
|
|
456
|
+
found_cuts.update(seen)
|
|
457
|
+
start_changes = [c for c in seen if c > s + 1e-3]
|
|
457
458
|
if start_changes:
|
|
458
459
|
cand = max(start_changes) # open after the last rapid cut in the window
|
|
459
460
|
if cand < e - min_keep:
|
|
@@ -463,11 +464,9 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
|
|
|
463
464
|
else:
|
|
464
465
|
event["start_unsnapped_reason"] = "collapse"
|
|
465
466
|
# Closing: a shot-change just BEFORE source_end leaves a next-shot sliver after it.
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
if c < e - 1e-3
|
|
470
|
-
]
|
|
467
|
+
seen = _detect_shot_changes(video, max(new_s, e - margin), e + pad, threshold)
|
|
468
|
+
found_cuts.update(seen)
|
|
469
|
+
end_changes = [c for c in seen if c < e - 1e-3]
|
|
471
470
|
if end_changes:
|
|
472
471
|
cand = min(end_changes) # close before the first rapid cut in the window
|
|
473
472
|
if cand > new_s + min_keep:
|
|
@@ -489,15 +488,57 @@ def snap_clips_off_shot_changes(plan, video, margin, threshold, min_keep=0.5):
|
|
|
489
488
|
return _plan_with_snapped_clips(plan, clips, "shot_snaps", events)
|
|
490
489
|
|
|
491
490
|
|
|
491
|
+
def _revert_unsafe_shot_snaps(plan, before, classify):
|
|
492
|
+
"""Undo a shot-change move that left an edge in speech where the plan's own edge was safe.
|
|
493
|
+
|
|
494
|
+
Sound outranks picture cleanup. Line snapping can pull a moved edge back out of speech
|
|
495
|
+
only next to a pause window, so an end the shot-change pass pulled back up to
|
|
496
|
+
`scene_margin` into a sentence (the plan ended it in an ASR gap that is music, not
|
|
497
|
+
silence) would block, and the nearest safe edge the gate suggests would be pulled back
|
|
498
|
+
again on the next run. Such an edge returns to `before` (the clips before the shot-change
|
|
499
|
+
pass) and its shot_snaps event reads `reverted_unsafe`, unless that would overlap
|
|
500
|
+
another clip's source range.
|
|
501
|
+
"""
|
|
502
|
+
events = plan.get("qc", {}).get("boundary_status", {}).get("shot_snaps", [])
|
|
503
|
+
if len(events) != len(before):
|
|
504
|
+
return plan
|
|
505
|
+
clips = [dict(c) for c in plan["clips"]]
|
|
506
|
+
events = [dict(e) for e in events]
|
|
507
|
+
reverted = 0
|
|
508
|
+
for idx, (clip, old, event) in enumerate(zip(clips, before, events)):
|
|
509
|
+
for edge, key, moved in (("start", "source_start", "moved_forward"),
|
|
510
|
+
("end", "source_end", "moved_back")):
|
|
511
|
+
if event[f"{edge}_action"] != moved:
|
|
512
|
+
continue
|
|
513
|
+
status, reason = classify(edge, clip[key], _edge_joined(clips, idx, edge))
|
|
514
|
+
if status != "blocking" or classify(
|
|
515
|
+
edge, old[key], _edge_joined(before, idx, edge))[0] != "safe":
|
|
516
|
+
continue
|
|
517
|
+
span = {**clip, key: old[key]}
|
|
518
|
+
if not plan["allow_overlap"] and _candidate_overlaps(
|
|
519
|
+
clips, idx, span["source_start"], span["source_end"]):
|
|
520
|
+
continue
|
|
521
|
+
clip[key] = old[key]
|
|
522
|
+
event[f"{edge}_action"] = "reverted_unsafe"
|
|
523
|
+
event[f"rejected_{edge}"] = event.pop(f"new_{edge}")
|
|
524
|
+
event[f"{edge}_revert_reason"] = reason
|
|
525
|
+
reverted += 1
|
|
526
|
+
if not reverted:
|
|
527
|
+
return plan
|
|
528
|
+
log(f"切镜头避让会截断原声,已撤回 {reverted} 处边界")
|
|
529
|
+
return _plan_with_snapped_clips(plan, clips, "shot_snaps", events)
|
|
530
|
+
|
|
531
|
+
|
|
492
532
|
def _load_silence_for_source(work_dir, source_id, source_work_dir=None):
|
|
493
533
|
"""Read a source's silence_periods.json (project layout when source_id is None); [] when absent."""
|
|
494
534
|
path = _find_source_artifact(work_dir, "silence_periods.json", source_id, source_work_dir)
|
|
495
535
|
return json.loads(path.read_text(encoding="utf-8")) if path else []
|
|
496
536
|
|
|
497
537
|
|
|
498
|
-
def
|
|
538
|
+
def snap_source_clips(
|
|
499
539
|
plan,
|
|
500
|
-
|
|
540
|
+
video,
|
|
541
|
+
duration,
|
|
501
542
|
work_dir,
|
|
502
543
|
*,
|
|
503
544
|
line_max_extend,
|
|
@@ -507,13 +548,68 @@ def snap_multi_source_clips(
|
|
|
507
548
|
start_max_trim,
|
|
508
549
|
do_line_snap=True,
|
|
509
550
|
do_scene_snap=True,
|
|
551
|
+
source_id=None,
|
|
552
|
+
source_work_dir=None,
|
|
553
|
+
frame_grid=None,
|
|
510
554
|
):
|
|
511
|
-
"""
|
|
555
|
+
"""Snap every clip of ONE source: shot changes, quiet starts/ends, frame grid, then the gate.
|
|
556
|
+
|
|
557
|
+
Visual cleanup goes first. Sentence/quiet snapping is the sound authority because a
|
|
558
|
+
clean picture is never allowed to reintroduce a mid-sentence audio cut (a shot-change
|
|
559
|
+
move line snapping cannot repair is reverted when the plan's own edge was gate-safe);
|
|
560
|
+
the frame-grid pass only moves an edge by under a frame and prefers the side that keeps
|
|
561
|
+
it safe, then the side that does not straddle a hard cut the shot-change scan saw, and
|
|
562
|
+
the gate judges the final, frame-aligned edges. source_id None reads the
|
|
563
|
+
project-level understanding artifacts (single-source layout). frame_grid is a
|
|
564
|
+
frame_grid.source_frame_grids() entry; None leaves edges off the grid.
|
|
565
|
+
"""
|
|
566
|
+
before_shot_snap = [dict(c) for c in plan["clips"]]
|
|
567
|
+
scene_cuts = set()
|
|
568
|
+
if do_scene_snap:
|
|
569
|
+
plan = snap_clips_off_shot_changes(
|
|
570
|
+
plan, video, margin=scene_margin, threshold=scene_threshold, found_cuts=scene_cuts
|
|
571
|
+
)
|
|
572
|
+
boundaries = _combine_boundary_windows(
|
|
573
|
+
_load_silence_for_source(work_dir, source_id, source_work_dir),
|
|
574
|
+
_load_sentence_boundary_windows(work_dir, source_id, source_work_dir),
|
|
575
|
+
)
|
|
576
|
+
if do_line_snap:
|
|
577
|
+
plan = snap_clip_starts_to_lines(
|
|
578
|
+
plan, boundaries, duration, start_max_prepend, max_trim=start_max_trim
|
|
579
|
+
)
|
|
580
|
+
plan = snap_clip_ends_to_lines(plan, boundaries, duration, line_max_extend)
|
|
581
|
+
speech_spans = _load_source_speech_spans(work_dir, source_id, source_work_dir)
|
|
582
|
+
video_start = frame_grid["origin"] if frame_grid is not None else 0.0
|
|
583
|
+
classify = _edge_classifier(boundaries, speech_spans, duration, _GATE_TOLERANCE,
|
|
584
|
+
video_start)
|
|
585
|
+
if do_scene_snap:
|
|
586
|
+
plan = _revert_unsafe_shot_snaps(plan, before_shot_snap, classify)
|
|
587
|
+
if frame_grid is None:
|
|
588
|
+
return enforce_clip_sentence_boundaries(plan, boundaries, speech_spans, duration)
|
|
589
|
+
|
|
590
|
+
frame_grid = {**frame_grid, "scene_cuts": sorted(scene_cuts)}
|
|
591
|
+
clips = plan["clips"]
|
|
592
|
+
joined = [idx > 0 and _continuous_source_join(clips[idx - 1], clip)
|
|
593
|
+
for idx, clip in enumerate(clips)]
|
|
594
|
+
clips, events = snap_edges_to_frames(
|
|
595
|
+
clips, frame_grid, boundaries, classify, duration,
|
|
596
|
+
allow_overlap=plan["allow_overlap"], joined_to_previous=joined,
|
|
597
|
+
)
|
|
598
|
+
plan = _plan_with_snapped_clips(plan, clips, "frame_snaps", events)
|
|
599
|
+
return enforce_clip_sentence_boundaries(
|
|
600
|
+
plan, boundaries, speech_spans, duration, video_start=video_start,
|
|
601
|
+
frame_snap=edge_frame_snapper(frame_grid, boundaries, classify, duration),
|
|
602
|
+
)
|
|
603
|
+
|
|
512
604
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
605
|
+
def snap_multi_source_clips(plan, sources, work_dir, frame_grids=None, **snap_options):
|
|
606
|
+
"""Per-source snap_source_clips for a multi-source validated plan.
|
|
607
|
+
|
|
608
|
+
Each clip is snapped using ITS OWN source's silence windows / shot changes, frame grid
|
|
609
|
+
and duration (a clip in source B never constrains a clip in source A), then the global
|
|
610
|
+
OUTPUT timeline is recomputed once in plan order. Missing silence data leaves a boundary
|
|
611
|
+
unchanged. `frame_grids` maps source_id to its frame grid; `snap_options` are
|
|
612
|
+
snap_source_clips' keyword options.
|
|
517
613
|
"""
|
|
518
614
|
clips = plan["clips"]
|
|
519
615
|
allow_overlap = plan["allow_overlap"]
|
|
@@ -524,34 +620,21 @@ def snap_multi_source_clips(
|
|
|
524
620
|
"start_snaps": [],
|
|
525
621
|
"end_snaps": [],
|
|
526
622
|
"shot_snaps": [],
|
|
623
|
+
"frame_snaps": [],
|
|
527
624
|
"sentence_checks": [],
|
|
528
625
|
}
|
|
529
626
|
blocking_accum = []
|
|
530
627
|
for sid, group in groups.items():
|
|
531
628
|
source = sources[sid]
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
)
|
|
540
|
-
|
|
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
|
-
)
|
|
545
|
-
if do_line_snap:
|
|
546
|
-
mini = snap_clip_starts_to_lines(
|
|
547
|
-
mini, boundaries, duration, start_max_prepend, max_trim=start_max_trim
|
|
548
|
-
)
|
|
549
|
-
mini = snap_clip_ends_to_lines(mini, boundaries, duration, line_max_extend)
|
|
550
|
-
mini = enforce_clip_sentence_boundaries(
|
|
551
|
-
mini,
|
|
552
|
-
boundaries,
|
|
553
|
-
_load_source_speech_spans(work_dir, sid, source_work_dir),
|
|
554
|
-
duration,
|
|
629
|
+
mini = snap_source_clips(
|
|
630
|
+
{"clips": [dict(c) for c in group], "allow_overlap": allow_overlap},
|
|
631
|
+
source["source_path"],
|
|
632
|
+
source["duration"],
|
|
633
|
+
work_dir,
|
|
634
|
+
source_id=sid,
|
|
635
|
+
source_work_dir=source.get("source_work_dir"),
|
|
636
|
+
frame_grid=(frame_grids or {}).get(sid),
|
|
637
|
+
**snap_options,
|
|
555
638
|
)
|
|
556
639
|
mini_boundary = mini["qc"]["boundary_status"]
|
|
557
640
|
for key, events in boundary_accum.items():
|