@oh-story/dsh 0.1.12 → 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
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import shlex
|
|
6
|
+
import shutil
|
|
6
7
|
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
from lib import load_json
|
|
11
|
+
from lib import file_identity, ffmpeg_filters, read_json_object
|
|
12
|
+
import resources.project_binding as project_binding
|
|
13
|
+
from library import LIBRARY_ENV
|
|
10
14
|
from recap_runtime import (
|
|
11
|
-
_coerce_videos,
|
|
12
15
|
_entry,
|
|
13
16
|
_load_run_manifest,
|
|
14
17
|
_multi_run_manifest_payload,
|
|
18
|
+
_resume_argv,
|
|
15
19
|
_run_manifest_payload,
|
|
16
20
|
)
|
|
17
|
-
from recap_source import audio_binding
|
|
21
|
+
from recap_source import audio_binding
|
|
18
22
|
|
|
19
23
|
ASSEMBLY_MANIFEST = "assembly_manifest.json"
|
|
20
24
|
|
|
@@ -66,6 +70,41 @@ def _canonical_visual_overlay(overlay, segment):
|
|
|
66
70
|
return item
|
|
67
71
|
|
|
68
72
|
|
|
73
|
+
def _canonical_visual_overlays(narration_path):
|
|
74
|
+
return [
|
|
75
|
+
item
|
|
76
|
+
for segment in load_json(narration_path)
|
|
77
|
+
for overlay in segment.get("visual_overlays", [])
|
|
78
|
+
if (item := _canonical_visual_overlay(overlay, segment)) is not None
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _preflight_visual_overlays(work_dir, *, narration):
|
|
83
|
+
"""Fail before TTS and render when the run will draw text overlays but this ffmpeg has
|
|
84
|
+
no drawtext (libfreetype; stock Homebrew ffmpeg lacks it). Overlays are authored content,
|
|
85
|
+
never a default, so they are not dropped silently the way a default subtitle burn
|
|
86
|
+
degrades: the message says how to remove them. Narration runs read narration.json (the
|
|
87
|
+
overlays are only written after TTS); source modes read an existing visual_overlays.json."""
|
|
88
|
+
work_dir = Path(work_dir)
|
|
89
|
+
if shutil.which("ffmpeg") is None:
|
|
90
|
+
return
|
|
91
|
+
if narration:
|
|
92
|
+
narration_path = work_dir / "narration.json"
|
|
93
|
+
overlays = _canonical_visual_overlays(narration_path) if narration_path.exists() else []
|
|
94
|
+
remedy = "删掉 narration.json 各段的 visual_overlays"
|
|
95
|
+
else:
|
|
96
|
+
data = read_json_object(work_dir / _VISUAL_OVERLAYS) or {}
|
|
97
|
+
overlays = data.get("overlays") if isinstance(data.get("overlays"), list) else []
|
|
98
|
+
remedy = f"删掉 {_VISUAL_OVERLAYS} 里的 overlays"
|
|
99
|
+
if not overlays or "drawtext" in ffmpeg_filters():
|
|
100
|
+
return
|
|
101
|
+
raise SystemExit(
|
|
102
|
+
f"本次运行有 {len(overlays)} 个画面文字叠加(visual_overlays),但当前 ffmpeg 不支持 drawtext"
|
|
103
|
+
" 滤镜(需要 libfreetype),会在最后渲染时失败。\n"
|
|
104
|
+
f" 解决其一:(1) 安装带 drawtext/libfreetype 的 ffmpeg;(2) {remedy} 后续跑。"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
69
108
|
def _write_canonical_visual_overlays(work_dir, narration_path):
|
|
70
109
|
"""Write assemble's canonical work_dir/visual_overlays.json recap handoff.
|
|
71
110
|
|
|
@@ -76,32 +115,13 @@ def _write_canonical_visual_overlays(work_dir, narration_path):
|
|
|
76
115
|
empty overlay list.
|
|
77
116
|
"""
|
|
78
117
|
path = Path(work_dir) / _VISUAL_OVERLAYS
|
|
79
|
-
overlays =
|
|
80
|
-
item
|
|
81
|
-
for segment in load_json(narration_path)
|
|
82
|
-
for overlay in segment.get("visual_overlays", [])
|
|
83
|
-
if (item := _canonical_visual_overlay(overlay, segment)) is not None
|
|
84
|
-
]
|
|
118
|
+
overlays = _canonical_visual_overlays(narration_path)
|
|
85
119
|
payload = {"schema_version": 1, "overlays": overlays}
|
|
86
120
|
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
87
121
|
print(f"[video-recap] 🧩 visual overlays: {len(overlays)} → {path}", flush=True)
|
|
88
122
|
return path
|
|
89
123
|
|
|
90
124
|
|
|
91
|
-
def _print_grounding_qc_pointer(work_dir):
|
|
92
|
-
qc_path = Path(work_dir) / "grounding_qc.json"
|
|
93
|
-
if not qc_path.exists():
|
|
94
|
-
return
|
|
95
|
-
data = load_json(qc_path)
|
|
96
|
-
ranges = data["review_coverage"]["time_ranges"]
|
|
97
|
-
warnings = data["warnings"]
|
|
98
|
-
suffix = f" · warnings {len(warnings)}" if warnings else ""
|
|
99
|
-
print(
|
|
100
|
-
f"[video-recap] 🧭 Grounding QC: {data['verdict']} · ranges {len(ranges)}{suffix} → {qc_path}",
|
|
101
|
-
flush=True,
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
|
|
105
125
|
def _print_narration_review_pointer(work_dir, *, review_ran=True):
|
|
106
126
|
"""Surface the advisory narration review produced by this run, if any.
|
|
107
127
|
|
|
@@ -109,7 +129,6 @@ def _print_narration_review_pointer(work_dir, *, review_ran=True):
|
|
|
109
129
|
stale narration_review.md from an older run is never surfaced. When it ran, video-script's
|
|
110
130
|
review_runner has written both narration_review.json and .md.
|
|
111
131
|
"""
|
|
112
|
-
_print_grounding_qc_pointer(work_dir)
|
|
113
132
|
if not review_ran:
|
|
114
133
|
return
|
|
115
134
|
review_md = Path(work_dir) / "narration_review.md"
|
|
@@ -191,20 +210,16 @@ def _read_assembly_output(work_dir):
|
|
|
191
210
|
|
|
192
211
|
|
|
193
212
|
def _read_phase_ledger(work_dir):
|
|
194
|
-
"""Phase ledger (cut mode):
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
prerequisite for the cut-first/narrate-second two-pause flow, and the guard that keeps a
|
|
198
|
-
narration written for one clip_plan from silently driving a different cut into TTS.
|
|
199
|
-
None before the first cut pass has recorded anything.
|
|
213
|
+
"""Phase ledger (cut mode with narration): the clip_plan identity the narration is
|
|
214
|
+
written against — the guard that keeps a narration written for one clip_plan from
|
|
215
|
+
silently driving a different cut into TTS. None before pass 2 has recorded it.
|
|
200
216
|
"""
|
|
201
217
|
path = Path(work_dir) / PHASE_LEDGER
|
|
202
218
|
return load_json(path) if path.exists() else None
|
|
203
219
|
|
|
204
220
|
|
|
205
|
-
def _write_phase_ledger(work_dir,
|
|
206
|
-
ledger =
|
|
207
|
-
ledger.update(fields)
|
|
221
|
+
def _write_phase_ledger(work_dir, clip_plan_identity):
|
|
222
|
+
ledger = {"clip_plan_identity": clip_plan_identity}
|
|
208
223
|
(Path(work_dir) / PHASE_LEDGER).write_text(
|
|
209
224
|
json.dumps(ledger, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
210
225
|
)
|
|
@@ -219,86 +234,9 @@ def _cut_narration_is_stale(ledger, current_clip_plan_identity):
|
|
|
219
234
|
return ledger is not None and ledger["clip_plan_identity"] != current_clip_plan_identity
|
|
220
235
|
|
|
221
236
|
|
|
222
|
-
def _continuation_command(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
parts = [
|
|
226
|
-
sys.executable,
|
|
227
|
-
str(_entry("video-recap", "recap.py")),
|
|
228
|
-
*[str(v) for v in _coerce_videos(video)],
|
|
229
|
-
"--work-dir",
|
|
230
|
-
str(work_dir),
|
|
231
|
-
]
|
|
232
|
-
if args.context:
|
|
233
|
-
parts += ["--context", args.context]
|
|
234
|
-
if args.scene_threshold is not None:
|
|
235
|
-
parts += ["--scene-threshold", str(args.scene_threshold)]
|
|
236
|
-
if args.style != "纪录片":
|
|
237
|
-
parts += ["--style", args.style]
|
|
238
|
-
if args.edit_mode != "full":
|
|
239
|
-
parts += ["--edit-mode", args.edit_mode]
|
|
240
|
-
if args.audio_mode != "narration":
|
|
241
|
-
parts += ["--audio-mode", args.audio_mode]
|
|
242
|
-
if args.audio_stream_index != 0:
|
|
243
|
-
parts += ["--audio-stream-index", str(args.audio_stream_index)]
|
|
244
|
-
if args.target_duration:
|
|
245
|
-
parts += ["--target-duration", args.target_duration]
|
|
246
|
-
if args.allow_duration_drift:
|
|
247
|
-
parts.append("--allow-duration-drift")
|
|
248
|
-
if args.skip_asr:
|
|
249
|
-
parts.append("--skip-asr")
|
|
250
|
-
if args.mimo_video_overview:
|
|
251
|
-
parts.append("--mimo-video-overview")
|
|
252
|
-
if args.mimo_qc != "off":
|
|
253
|
-
parts += ["--mimo-qc", args.mimo_qc]
|
|
254
|
-
if args.mimo_qc_refresh:
|
|
255
|
-
parts.append("--mimo-qc-refresh")
|
|
256
|
-
if not args.consolidate: # default is ON; only the opt-out needs to round-trip
|
|
257
|
-
parts.append("--no-consolidate")
|
|
258
|
-
if args.consolidate_asr:
|
|
259
|
-
parts.append("--consolidate-asr")
|
|
260
|
-
if uses_narration(args):
|
|
261
|
-
if args.mimo_tts_voice and "mimo_tts_voice" not in bound:
|
|
262
|
-
parts += ["--mimo-tts-voice", args.mimo_tts_voice]
|
|
263
|
-
if args.tts_provider != "auto" and "tts_provider" not in bound:
|
|
264
|
-
parts += ["--tts-provider", args.tts_provider]
|
|
265
|
-
if args.voice_ref and "voice_ref" not in bound:
|
|
266
|
-
parts += ["--voice-ref", args.voice_ref]
|
|
267
|
-
if args.allow_partial_tts:
|
|
268
|
-
parts.append("--allow-partial-tts")
|
|
269
|
-
if args.preserve_approved_text:
|
|
270
|
-
parts.append("--preserve-approved-text")
|
|
271
|
-
if args.burn_subtitles is not None:
|
|
272
|
-
parts.append("--burn-subtitles" if args.burn_subtitles else "--no-burn-subtitles")
|
|
273
|
-
if args.subtitle_y_top is not None:
|
|
274
|
-
parts += ["--subtitle-y-top", str(args.subtitle_y_top)]
|
|
275
|
-
if args.subtitle_y_bot is not None:
|
|
276
|
-
parts += ["--subtitle-y-bot", str(args.subtitle_y_bot)]
|
|
277
|
-
if args.output_dir:
|
|
278
|
-
parts += ["--output-dir", args.output_dir]
|
|
279
|
-
if args.export_jianying:
|
|
280
|
-
parts.append("--export-jianying")
|
|
281
|
-
if args.jianying_bundle_media:
|
|
282
|
-
parts.append("--jianying-bundle-media")
|
|
283
|
-
if args.jianying_no_bundle_media:
|
|
284
|
-
parts.append("--jianying-no-bundle-media")
|
|
285
|
-
if uses_narration(args):
|
|
286
|
-
if args.review_narration is not None:
|
|
287
|
-
parts.append(
|
|
288
|
-
"--review-narration" if args.review_narration else "--no-review-narration"
|
|
289
|
-
)
|
|
290
|
-
if args.require_narration_review:
|
|
291
|
-
parts.append("--require-narration-review")
|
|
292
|
-
if args.material_library_dir and "material_library_dir" not in bound:
|
|
293
|
-
parts += ["--material-library-dir", args.material_library_dir]
|
|
294
|
-
if getattr(args, "project", None):
|
|
295
|
-
parts += ["--project", args.project]
|
|
296
|
-
if args.use_materials:
|
|
297
|
-
parts.append("--use-materials")
|
|
298
|
-
if args.save_materials:
|
|
299
|
-
parts.append("--save-materials")
|
|
300
|
-
if args.require_final_qc:
|
|
301
|
-
parts.append("--require-final-qc")
|
|
237
|
+
def _continuation_command(work_dir, args):
|
|
238
|
+
"""Shell command that resumes this run: the original argv, replayable from any cwd."""
|
|
239
|
+
parts = [sys.executable, str(_entry("video-recap", "recap.py")), *_resume_argv(work_dir, args)]
|
|
302
240
|
return " ".join(shlex.quote(part) for part in parts)
|
|
303
241
|
|
|
304
242
|
|
|
@@ -454,26 +392,57 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
|
454
392
|
)
|
|
455
393
|
return cache[source_id]
|
|
456
394
|
|
|
395
|
+
spans_by_source = {}
|
|
396
|
+
for clip in plan["clips"]:
|
|
397
|
+
spans_by_source.setdefault(clip["source_id"], []).append(
|
|
398
|
+
(float(clip["source_start"]), float(clip["source_end"])))
|
|
399
|
+
|
|
457
400
|
mapped_anchors, mapped_speech, mapped_quiet = [], [], []
|
|
401
|
+
output_duration = max(float(clip["output_end"]) for clip in plan["clips"])
|
|
458
402
|
for clip in plan["clips"]:
|
|
459
403
|
source_id = clip["source_id"]
|
|
460
404
|
source_start = float(clip["source_start"])
|
|
461
405
|
source_end = float(clip["source_end"])
|
|
462
406
|
output_start = float(clip["output_start"])
|
|
407
|
+
output_end = float(clip["output_end"])
|
|
408
|
+
|
|
409
|
+
def to_output(when):
|
|
410
|
+
# A source time on this clip's OUTPUT clock, never past its own span: the next
|
|
411
|
+
# clip on the output timeline plays a different range (or source).
|
|
412
|
+
when = min(max(when, source_start), source_end)
|
|
413
|
+
return round(min(output_start + when - source_start, output_end), 3)
|
|
414
|
+
|
|
463
415
|
anchors, speech_rows, quiet_rows = load_source(source_id)
|
|
464
416
|
for anchor in anchors:
|
|
465
417
|
when = float(anchor["time"])
|
|
466
418
|
if not (source_start - 0.05 <= when <= source_end + 0.05):
|
|
467
419
|
continue
|
|
468
|
-
|
|
420
|
+
# The 0.05 s source slack can carry an anchor past the output's ends (a clip
|
|
421
|
+
# end frame-snapped 181.42 -> 181.40): narration cannot start there.
|
|
422
|
+
if not 0 <= round(output_start + when - source_start, 3) <= output_duration:
|
|
423
|
+
continue
|
|
424
|
+
# A pause end just past a frame-snapped edge belongs to this clip at that edge,
|
|
425
|
+
# unless another clip of the same source plays that instant itself.
|
|
426
|
+
if not source_start <= when <= source_end and any(
|
|
427
|
+
start <= when <= end for start, end in spans_by_source[source_id]):
|
|
428
|
+
continue
|
|
429
|
+
# Same default as the single-source remap in video-understanding's timeline brief.
|
|
430
|
+
pause = max(source_start, min(float(anchor.get("pause_start", when - 0.12)), when))
|
|
469
431
|
item = dict(anchor)
|
|
470
432
|
item.update(
|
|
471
433
|
source_id=source_id,
|
|
472
434
|
source_time=round(when, 3),
|
|
473
|
-
time=
|
|
474
|
-
source_pause_start=round(pause, 3),
|
|
475
|
-
pause_start=
|
|
435
|
+
time=to_output(when),
|
|
436
|
+
source_pause_start=round(min(pause, source_end), 3),
|
|
437
|
+
pause_start=to_output(pause),
|
|
438
|
+
# `time` IS the pause end; keep the source value apart (one clock per field).
|
|
439
|
+
source_pause_end=round(float(anchor.get("pause_end", when)), 3),
|
|
476
440
|
)
|
|
441
|
+
item["pause_end"] = item["time"]
|
|
442
|
+
if "expected_time" in anchor:
|
|
443
|
+
expected = float(anchor["expected_time"])
|
|
444
|
+
item["source_expected_time"] = round(expected, 3)
|
|
445
|
+
item["expected_time"] = to_output(expected)
|
|
477
446
|
mapped_anchors.append(item)
|
|
478
447
|
for rows, destination, require_text in (
|
|
479
448
|
(speech_rows, mapped_speech, True),
|
|
@@ -493,8 +462,8 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
|
493
462
|
source_id=source_id,
|
|
494
463
|
source_start=round(start, 3),
|
|
495
464
|
source_end=round(end, 3),
|
|
496
|
-
start=
|
|
497
|
-
end=
|
|
465
|
+
start=to_output(start),
|
|
466
|
+
end=to_output(end),
|
|
498
467
|
)
|
|
499
468
|
destination.append(item)
|
|
500
469
|
|
|
@@ -503,6 +472,8 @@ def _write_multi_source_output_speech_evidence(work_dir, source_records, plan):
|
|
|
503
472
|
"artifact": "speech_boundary_anchors_output.json",
|
|
504
473
|
"timeline": "cut_output",
|
|
505
474
|
"source_artifact": "multi_source_manifest.json",
|
|
475
|
+
# video-script's cut_output lint trusts this evidence only for this exact plan file.
|
|
476
|
+
"clip_plan_identity": file_identity(Path(work_dir) / "clip_plan_validated.json"),
|
|
506
477
|
"sentence_anchors": sorted(mapped_anchors, key=lambda row: row["time"]),
|
|
507
478
|
"speech_spans": sorted(mapped_speech, key=lambda row: (row["start"], row["end"])),
|
|
508
479
|
"quiet_windows": sorted(mapped_quiet, key=lambda row: (row["start"], row["end"])),
|
|
@@ -552,14 +523,31 @@ def _write_multi_source_output_brief(work_dir, source_records, validated_plan_pa
|
|
|
552
523
|
f"{c['source_id']} `{src['source_path']}` "
|
|
553
524
|
f"source {_fmt_range(c['source_start'], c['source_end'])}{reason}"
|
|
554
525
|
)
|
|
555
|
-
anchors =
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if row["confidence"] in {"high", "medium"}
|
|
526
|
+
anchors = []
|
|
527
|
+
for row in speech_evidence["sentence_anchors"]:
|
|
528
|
+
# Schema-1 anchors (no `boundary_use`) came from the old coarse estimator: high/medium
|
|
529
|
+
# labels there are usable but unverified.
|
|
530
|
+
use = row.get("boundary_use") or (
|
|
531
|
+
"unverified" if row["confidence"] in {"high", "medium"} else "none"
|
|
562
532
|
)
|
|
533
|
+
if use != "none":
|
|
534
|
+
anchors.append((row, use))
|
|
535
|
+
if anchors:
|
|
536
|
+
lines += [
|
|
537
|
+
"",
|
|
538
|
+
"## 原声句末安全切入点",
|
|
539
|
+
"",
|
|
540
|
+
"`unverified` 点是粗粒度 ASR 窗口里的标点位置估计、再吸附到短停顿:不切断单词,"
|
|
541
|
+
"但不保证原声句子已说完(误差上限见 `±`);门禁照常生效。",
|
|
542
|
+
]
|
|
543
|
+
for row, use in anchors:
|
|
544
|
+
if use == "verified":
|
|
545
|
+
label = row["confidence"]
|
|
546
|
+
elif "timing_bound_seconds" in row:
|
|
547
|
+
label = f"unverified ±{float(row['timing_bound_seconds']):.1f}s"
|
|
548
|
+
else: # schema-1 anchors carry no error bound
|
|
549
|
+
label = "unverified"
|
|
550
|
+
lines.append(f"- {row['time']:.3f}s [{label}] ({row['source_id']})")
|
|
563
551
|
lines += ["", "## Source work dirs"]
|
|
564
552
|
for s in source_records:
|
|
565
553
|
lines.append(f"- {s['source_id']}: `{_source_work_dir(work_dir, s)}`")
|
|
@@ -595,7 +583,7 @@ def _fmt_range(start, end):
|
|
|
595
583
|
|
|
596
584
|
|
|
597
585
|
def _material_library_dir(args):
|
|
598
|
-
return args.material_library_dir or os.environ.get(
|
|
586
|
+
return args.material_library_dir or os.environ.get(LIBRARY_ENV) or None
|
|
599
587
|
|
|
600
588
|
|
|
601
589
|
def _materials_enabled(args):
|
|
@@ -606,7 +594,7 @@ def _save_materials_enabled(args):
|
|
|
606
594
|
return bool(_material_library_dir(args) and args.save_materials)
|
|
607
595
|
|
|
608
596
|
|
|
609
|
-
def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
|
|
597
|
+
def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None, project=None):
|
|
610
598
|
brief = Path(work_dir) / "agent_narration_brief.md"
|
|
611
599
|
print("=" * 50)
|
|
612
600
|
if "Research the story FIRST" in brief.read_text(encoding="utf-8"):
|
|
@@ -617,5 +605,8 @@ def _pause_for_agent(work_dir, need_text, cont, inspect_hint=None):
|
|
|
617
605
|
print(f"[video-recap] ⏸ 阅读 {brief}(按 video-script 规则)后写入 {need_text}")
|
|
618
606
|
if inspect_hint:
|
|
619
607
|
print(f"[video-recap] 先核对状态/时间轴(建议性): {inspect_hint}")
|
|
608
|
+
reference_note = project_binding.bound_reference_note(work_dir, project)
|
|
609
|
+
if reference_note:
|
|
610
|
+
print(f"[video-recap] {reference_note}")
|
|
620
611
|
print(f"[video-recap] 写完后重跑继续: {cont}")
|
|
621
612
|
print("=" * 50)
|
|
@@ -12,7 +12,7 @@ from datetime import datetime, timezone
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
14
|
import library as library_lib
|
|
15
|
-
from
|
|
15
|
+
from lib import file_identity
|
|
16
16
|
|
|
17
17
|
LOCK_NAME = "resource_lock.json"
|
|
18
18
|
LOCK_SCHEMA = "video-recap.resource-lock.v1"
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Only video-recap reads the library. A bound subtitle style becomes ``SUBTITLE_*`` env for
|
|
4
4
|
video-assemble, a bound voice becomes the voiceover provider / voice arguments, a bound
|
|
5
|
-
BGM becomes ``BGM_PATH
|
|
5
|
+
BGM becomes ``BGM_PATH``, a bound production reference becomes a marked copy in the work_dir
|
|
6
|
+
that only the writing agent reads. Stage skills keep seeing concrete paths and values; they never
|
|
6
7
|
learn about templates. Anything the caller already set explicitly must agree with the
|
|
7
8
|
binding — a silent override would make the delivered file disagree with the project.
|
|
8
9
|
"""
|
|
@@ -17,7 +18,8 @@ import library as library_lib
|
|
|
17
18
|
PROJECT_FILE = "recap_project.json"
|
|
18
19
|
PROJECT_SCHEMA = "video-recap.project.v1"
|
|
19
20
|
PROJECT_KEYS = {"schema", "name", "library", "bindings", "notes"}
|
|
20
|
-
BINDING_KINDS = ("subtitle_style", "packaging", "voice", "bgm")
|
|
21
|
+
BINDING_KINDS = ("subtitle_style", "packaging", "voice", "bgm", "production_reference")
|
|
22
|
+
GEOMETRY_ROLES = {"subtitle_style", "packaging"}
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class BindingError(SystemExit):
|
|
@@ -188,6 +190,14 @@ def resolve_project(value, args, environ=None, *, include_voice=True) -> dict:
|
|
|
188
190
|
used_templates.append({"role": "packaging", "id": packaging["id"],
|
|
189
191
|
"version": packaging["version"], "status": packaging["status"],
|
|
190
192
|
"canvas": packaging["canvas"]})
|
|
193
|
+
reference = template("production_reference")
|
|
194
|
+
if reference:
|
|
195
|
+
used_templates.append({"role": "production_reference", "id": reference["id"],
|
|
196
|
+
"version": reference["version"], "status": reference["status"],
|
|
197
|
+
"canvas": reference["canvas"]})
|
|
198
|
+
reference = {"template": {"id": reference["id"], "version": reference["version"]},
|
|
199
|
+
"payload": json.loads((Path(reference["record"]).parent / library_lib.REFERENCE_FILE)
|
|
200
|
+
.read_text(encoding="utf-8"))}
|
|
191
201
|
voice = resource("voice") if include_voice else None
|
|
192
202
|
if voice:
|
|
193
203
|
voice_updates, voice_env = _voice_updates(voice, args)
|
|
@@ -221,6 +231,7 @@ def resolve_project(value, args, environ=None, *, include_voice=True) -> dict:
|
|
|
221
231
|
"templates": used_templates,
|
|
222
232
|
"resources": [],
|
|
223
233
|
"packaging": packaging and {"record": _record(packaging), "resources": resources},
|
|
234
|
+
"production_reference": reference,
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
|
|
@@ -244,6 +255,8 @@ def apply_project(resolved, args, environ=None) -> None:
|
|
|
244
255
|
def check_canvas(resolved, width, height) -> None:
|
|
245
256
|
"""Templates only apply to the canvas they were calibrated on."""
|
|
246
257
|
for template in resolved["templates"]:
|
|
258
|
+
if template["role"] not in GEOMETRY_ROLES: # a production reference carries no geometry
|
|
259
|
+
continue
|
|
247
260
|
canvas = template["canvas"]
|
|
248
261
|
if (canvas["width"], canvas["height"]) != (width, height):
|
|
249
262
|
raise BindingError(
|
|
@@ -253,34 +266,82 @@ def check_canvas(resolved, width, height) -> None:
|
|
|
253
266
|
|
|
254
267
|
|
|
255
268
|
PACKAGING_LAYERS = "packaging_layers.json"
|
|
269
|
+
REFERENCE_COPY = "production_reference.json"
|
|
256
270
|
_WRITTEN_BY = "video-recap --project"
|
|
257
271
|
|
|
258
272
|
|
|
259
|
-
def
|
|
260
|
-
|
|
273
|
+
def _read_json(path):
|
|
274
|
+
try:
|
|
275
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
276
|
+
except (OSError, ValueError):
|
|
277
|
+
return None
|
|
278
|
+
|
|
261
279
|
|
|
262
|
-
|
|
280
|
+
def _ours(data) -> bool:
|
|
281
|
+
return isinstance(data, dict) and data.get("written_by") == _WRITTEN_BY
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _sync_bound_file(work_dir, name, payload) -> None:
|
|
285
|
+
"""Write ``payload`` with our marker, or retire our old copy when nothing is bound.
|
|
286
|
+
|
|
287
|
+
A caller-authored file (no ``written_by`` marker) is never deleted.
|
|
263
288
|
"""
|
|
264
|
-
path = Path(work_dir) /
|
|
289
|
+
path = Path(work_dir) / name
|
|
290
|
+
if payload is None:
|
|
291
|
+
if path.exists() and _ours(_read_json(path)):
|
|
292
|
+
path.unlink()
|
|
293
|
+
return
|
|
294
|
+
# The marker goes last so a payload key of the same name can never unmark our copy.
|
|
295
|
+
path.write_text(json.dumps({**payload, "written_by": _WRITTEN_BY}, ensure_ascii=False, indent=2),
|
|
296
|
+
encoding="utf-8")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def sync_packaging_layers(work_dir, resolved) -> None:
|
|
300
|
+
"""Write the bound packaging template's layers for video-assemble, or retire our old copy."""
|
|
265
301
|
packaging = (resolved or {}).get("packaging")
|
|
266
302
|
if not packaging:
|
|
267
|
-
|
|
268
|
-
try:
|
|
269
|
-
plan = json.loads(path.read_text(encoding="utf-8"))
|
|
270
|
-
except ValueError:
|
|
271
|
-
plan = None
|
|
272
|
-
ours = isinstance(plan, dict) and plan.get("written_by") == _WRITTEN_BY
|
|
273
|
-
if ours:
|
|
274
|
-
path.unlink()
|
|
303
|
+
_sync_bound_file(work_dir, PACKAGING_LAYERS, None)
|
|
275
304
|
return
|
|
276
305
|
record, resources = packaging["record"], packaging["resources"]
|
|
277
306
|
layers = [{"name": layer["name"],
|
|
278
307
|
"path": resources[layer["image"]["resource"]]["files"][0]["path"],
|
|
279
308
|
"rect": layer["rect"]}
|
|
280
309
|
for layer in record["params"]["layers"]]
|
|
281
|
-
|
|
282
|
-
"written_by": _WRITTEN_BY,
|
|
310
|
+
_sync_bound_file(work_dir, PACKAGING_LAYERS, {
|
|
283
311
|
"template": {"id": record["id"], "version": record["version"]},
|
|
284
312
|
"canvas": record["canvas"],
|
|
285
313
|
"layers": layers,
|
|
286
|
-
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def sync_production_reference(work_dir, resolved) -> None:
|
|
318
|
+
"""Hand the bound reference to the writing agent before its first pause; no script reads it.
|
|
319
|
+
|
|
320
|
+
A caller's own copy is never rewritten: identical to the binding it is left as is, different
|
|
321
|
+
from it is a conflict, like any other setting.
|
|
322
|
+
"""
|
|
323
|
+
bound = (resolved or {}).get("production_reference")
|
|
324
|
+
path = Path(work_dir) / REFERENCE_COPY
|
|
325
|
+
if bound and path.exists():
|
|
326
|
+
current = _read_json(path)
|
|
327
|
+
if not _ours(current):
|
|
328
|
+
if current == bound["payload"]:
|
|
329
|
+
return
|
|
330
|
+
template = bound["template"]
|
|
331
|
+
raise BindingError(f"{path} 不是 --project 写的,与绑定的 {template['id']}@v{template['version']} "
|
|
332
|
+
"不同;删掉其中一处")
|
|
333
|
+
_sync_bound_file(work_dir, REFERENCE_COPY,
|
|
334
|
+
bound and {**bound["payload"], "template": bound["template"]})
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def bound_reference_note(work_dir, resolved=None) -> str | None:
|
|
338
|
+
"""The pause-banner line for the bound reference in work_dir (our copy or the caller's identical one)."""
|
|
339
|
+
data = _read_json(Path(work_dir) / REFERENCE_COPY)
|
|
340
|
+
bound = (resolved or {}).get("production_reference")
|
|
341
|
+
if _ours(data):
|
|
342
|
+
template = data.get("template") or {}
|
|
343
|
+
elif bound and data == bound["payload"]:
|
|
344
|
+
template = bound["template"]
|
|
345
|
+
else:
|
|
346
|
+
return None
|
|
347
|
+
return f"本轮带制作参考 {template.get('id')}@v{template.get('version')}(可选,取舍写入 reference_methods)"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-reference
|
|
3
|
+
user-invocable: true
|
|
4
|
+
description: >
|
|
5
|
+
按需把一部成片拆成可复用的制作参考:测镜头节奏与响度,标注段落与音轨分工,把原片事实与可迁移方法分开,
|
|
6
|
+
导出不含原片人名台词的 production_reference.json 供下次制作参考。不在默认生产路径上。
|
|
7
|
+
触发词:拆片、拆解成片、制作参考、参考模板、production reference。
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. 定位
|
|
11
|
+
|
|
12
|
+
本技能把一部**已完成的成片**拆成下一次制作能直接参考的方法与数值。它是按需的参考分析,不是生产阶段,也不是质检:
|
|
13
|
+
不调用 MiMo,不改其他产物,不给任何运行打分或拦截。
|
|
14
|
+
|
|
15
|
+
Agent 的角色是**拆片编辑**:先如实记录"这部片子怎么做的"(事实),再提炼"换一部素材还能怎么用"(方法)。
|
|
16
|
+
两者必须分开:事实只留在本地工作目录,导出物只含方法和测得的数值。
|
|
17
|
+
|
|
18
|
+
## 2. 输入
|
|
19
|
+
|
|
20
|
+
- 成片文件(`.mp4 / .mov / .mkv / .webm`)。
|
|
21
|
+
- 对**这部成片**跑出的视频理解产物目录 `U`(建议 `ASR_SEGMENT_SECONDS=5`,窗口越短,旁白语速越准)。
|
|
22
|
+
本技能读取其中可选的 `asr_result.json`、`asr_timing_evidence.json`、`background_research.json`、`understanding_index.json`
|
|
23
|
+
(其 `characters` 的名字、别名与 ASR 提及都进入人名泄漏扫描);
|
|
24
|
+
标注时 Agent 还应看故事板 / contact sheet 与 `vlm_analysis.json`。
|
|
25
|
+
|
|
26
|
+
没有理解产物也能跑,但旁白语速为空,泄漏扫描只剩 Agent 自己写的 `entities`,check 会给出警告;
|
|
27
|
+
看画面改用 `frames --span 0,<时长> --step 2` 的接触表。
|
|
28
|
+
|
|
29
|
+
下面的 `scripts/...` 均相对于本技能目录。
|
|
30
|
+
|
|
31
|
+
## 3. 流程
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 scripts/reference.py measure <成片> --work-dir U # 一次 ffmpeg:切点 + 响度,按文件身份缓存
|
|
35
|
+
python3 scripts/reference.py frames <成片> --work-dir U --review # 逐帧看被压下的疑似切点
|
|
36
|
+
python3 scripts/reference.py frames <成片> --work-dir U --longest 5 # 看最长镜头里有没有漏切
|
|
37
|
+
python3 scripts/reference.py check --work-dir U [--json] # 校验并打印派生值
|
|
38
|
+
python3 scripts/reference.py export --work-dir U --out <下次运行的 work_dir>/production_reference.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
1. **measure** 写 `U/reference_measurements.json`(只由脚本写):镜头切点、镜长分布、每分钟切点数、10 秒切点曲线、
|
|
42
|
+
整体响度 / LRA / 真峰值、逐秒短期响度。它要解码整片:5 分钟 720p 约 15–70 秒,4K 约 7 分钟;成片不变时复用,
|
|
43
|
+
只改 `--soft-score/--hard-score` 不重新解码。切点规则:scdet 分数 ≥10 必算;≥4 且是前后 0.3 秒内其他帧(相邻帧除外)
|
|
44
|
+
两倍以上的孤立峰才算。运动镜头、急推拉、闪光会被压下,并列进 `shots.review_windows`;scdet 分数会减去前一帧的帧差,
|
|
45
|
+
从快速运动切进静止镜头的硬切可能只有 1 分,所以帧差本身的单侧峰(`mafd_peaks`)也进待复核窗口,但不会自动算切点。
|
|
46
|
+
2. **复核切点**:固定阈值在真实成片上两头都错(暗场硬切只有 5–7 分;快速运动的单个镜头每 0.2 秒一个 7–8 分的峰),
|
|
47
|
+
所以每次都要看图。`frames --review` 逐帧拼出每个待复核窗口,`frames --longest 5` 在最长的 5 个镜头里均匀取 12 帧;
|
|
48
|
+
页面在 `U/reference_frames/`,命令打印每格对应的秒数。看完写 `labels.cut_fixes`:`add` 漏掉的切点秒数,
|
|
49
|
+
`remove` 误报的切点秒数(±0.1 秒内对上测得的切点);看过无需改动就写 `{}`。重新 `measure` 后待复核窗口数变了,旧的 `cut_fixes` 不再算数,按新窗口重看一遍。之后所有镜头数值、段内切点密度和导出都用复核后的切点。
|
|
50
|
+
3. **标注**:Agent 写 `U/reference_breakdown.json` 的 `labels`——音轨归属 `audio_spans`、叙事段落 `sections`、
|
|
51
|
+
字幕形态 `subtitles`、标注依据 `basis`。`audio_spans` 的边界放在**声音实际起止**处(听得到的人声起点与止点),
|
|
52
|
+
不放在字幕或旁白块的开始处(按字幕出现帧定的旁白结束点实测晚 0.24–0.42 秒)。`U/speech_boundary_anchors.json` 存在时,
|
|
53
|
+
用它的 `acoustic_pauses`(`start` 是人声停下处,`end` 是下一句开口处)对齐边界;`switch_on_cut_share` 只容差 ±0.25 秒,
|
|
54
|
+
边界放错它就只反映标注习惯。原片自带的画外音、内心独白也是原片音轨,记 `original_dialogue`;要区分时写进 fact 和方法。
|
|
55
|
+
只有 labels 时就可以跑 check,它会打印 `derived`(各音轨占比、段内切点密度、
|
|
56
|
+
旁白语速、声音切换与画面切点的对齐比例、分数位置的结构、第一次原声出现位置)。
|
|
57
|
+
4. **事实与方法**:在同一文件写 `source_facts`(带时间或测量锚点、显式 `entities`)和 `methods`
|
|
58
|
+
(`rule`、`applies_when`、`avoid_when`、`applies_to`、`evidence`、`targets`)。`targets` 只写 `{"from": "<测量路径>"}`,
|
|
59
|
+
数值由 export 从当前测量填入,Agent 永远不手写数字。
|
|
60
|
+
5. **check → export**:零 error 才导出;导出后对产物再做一次泄漏扫描。
|
|
61
|
+
|
|
62
|
+
字段、枚举和好/坏方法示例见 `references/reference-schema.md`。
|
|
63
|
+
|
|
64
|
+
## 4. 分离规则
|
|
65
|
+
|
|
66
|
+
check 的 error(退出码 1):
|
|
67
|
+
|
|
68
|
+
| 规则 | 要求 |
|
|
69
|
+
|---|---|
|
|
70
|
+
| R1 | 顶层、labels、fact、method、target、`cut_fixes` 都是封闭键集与封闭枚举;fact 不能带方法字段,method 不能带事实字段;id 为 `f1…` / `m1…` 且唯一;`subtitles` 值类型固定;`skipped_dimensions` 的值都是非空字符串 |
|
|
71
|
+
| R2 | `audio_spans`、`sections` 按时间排序、不重叠、间隙 ≤0.5s、覆盖整片;字幕证据时间在时长内;`cut_fixes.remove` 对得上测得的切点,`add` 不与测得的切点重复 |
|
|
72
|
+
| R3 | 每条 fact 二选一锚定:`t:[a,b]` 在时长内,或 `measure:[路径]` 解析到 `shots` / `loudness` / `derived` 下的非字符串值;必须显式写 `entities` |
|
|
73
|
+
| R4 | 每条 method 至少一条证据:已有 fact id 或同样只认这三个根的 `measure:<路径>` |
|
|
74
|
+
| R5 | target 只写 `from`:`shots` / `loudness` / `derived` 下的数值叶子,或白名单派生对象(见 schema);不得带列表下标 |
|
|
75
|
+
| R6 | `rule` / `applies_when` / `avoid_when` 和 `skipped_dimensions` 的原因不得含:原片实体名(忽略空白)、与台词、背景资料或事实共有的连续 8 个汉字(标点隔开、跨相邻 ASR 窗口也算)或 5 个英文词、绝对时间码、"第 N 秒"或"N 分 M 秒"(含中文数字)、绝对路径 |
|
|
76
|
+
| R7 | 五个维度各至少一条 method,或在 `skipped_dimensions` 写明原因 |
|
|
77
|
+
| R8 | 导出物的每个键和字符串再扫一遍 R6,且不得出现 `source_facts`、`labels`、`entities`、`evidence`、`statement`、`from`、`path` 键 |
|
|
78
|
+
|
|
79
|
+
实体名来自 fact `entities`、`understanding_index.json` 的 `characters` / `entities` / `research_glossary` 名字与别名、`background_research.json` 的角色与
|
|
80
|
+
`character_details` 别名、≤5 字的 `cultural_notes` 条目,以及资料里《》「」引号中的短词。与事实共有 8 字时错误会注明来自
|
|
81
|
+
source_facts:通用剪辑措辞改写任一侧即可。
|
|
82
|
+
|
|
83
|
+
警告不阻断:method 缺 `applies_when`、rule 正文写了数字、ASR 不是 `AVAILABLE_COARSE`、有中文对白但理解产物与背景资料都没给出名字(此时只扫 fact `entities`)、`basis` 为空、
|
|
84
|
+
理解产物记录的成片身份与测量的成片不一致、有待复核窗口却没写 `cut_fixes`。
|
|
85
|
+
|
|
86
|
+
R6 只能拦住字面泄漏,拦不住改写过的剧情;写方法时要写"什么情况下怎么做",不要复述"这部片里发生了什么"。
|
|
87
|
+
规则也不判断语义:方法的方向必须与 check 打印的派生值一致(例如先看各音轨的 `cuts_per_min` 再写"哪类段落切得密")。
|
|
88
|
+
|
|
89
|
+
## 5. 交付与使用
|
|
90
|
+
|
|
91
|
+
`production_reference.json` 只含:时长与画布、`cut_detection`(切点规则的参数与 Agent 增删的切点数)、
|
|
92
|
+
`profile`(节奏数值,`provenance` 为 `measured` / `reviewed`(写了 `cut_fixes`,`{}` 也算)/ `labeled`)、
|
|
93
|
+
分数位置的 `structure`、字幕形态、`methods`、`skipped_dimensions`。
|
|
94
|
+
|
|
95
|
+
使用方式:把它复制进下一次制作的 `work_dir`。写稿阶段只在文件存在时阅读它,
|
|
96
|
+
并可在 `recap_story_plan.json` 的可选字段 `reference_methods` 记录每条方法的 adopt / adapt / skip。
|
|
97
|
+
它是参考,不是配额:与新素材的证据冲突时以素材为准。没有任何脚本读取它,也不会因为它的存在改变任何阶段的通过与否。
|
|
98
|
+
|
|
99
|
+
`U/` 里的 breakdown 与 `reference_frames/` 含原片事实和画面,只留在本地,不要复制或提交。
|
|
100
|
+
|
|
101
|
+
## 6. 能力边界
|
|
102
|
+
|
|
103
|
+
- 不调用 MiMo 或任何 LLM,不改动理解产物,不给成片打分,不做"新片与参考的差距"判定。
|
|
104
|
+
- 切点来自 ffmpeg `scdet` 分数:慢叠化测不到;相隔 0.3 秒内分数相近的两个真切点、刚好压在 soft 线上的切点(不同 ffmpeg 构建
|
|
105
|
+
4.00 与 3.989 分)、运动后的暗场硬切(0.9 分)都不算切点,只进待复核窗口。在一部 5 分钟剧集解说上测得 82 个切点,
|
|
106
|
+
11 个待复核窗口里 4 个含漏掉的 5 个真切点,其余是急推拉、翻页和运动镜头;补上后,三段逐帧真值(开场、暗场、中段共 27 个)全部对上、无误报。
|
|
107
|
+
- 旁白占比与语速是 `labeled` 精度:来自 Agent 的音轨标注与粗 ASR 窗口,不是逐词对齐。
|
|
108
|
+
- 泄漏扫描基于字面匹配,Agent 仍需自查方法是否在复述原片剧情。
|