@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Load measured source-speech evidence and classify narration ownership."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from lib import CONFIG, file_identity
|
|
@@ -10,16 +11,93 @@ def _empty_evidence(mode):
|
|
|
10
11
|
return {
|
|
11
12
|
"anchors": [],
|
|
12
13
|
"speech_spans": [],
|
|
14
|
+
"dialogue_spans": [],
|
|
13
15
|
"quiet_windows": [],
|
|
14
16
|
"require_measured": mode == "cut_output",
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
# Interjections and common ASR artifacts on screams/music. A window whose text is only these
|
|
21
|
+
# is not dialogue at a clip edge or narration entry; real short lines such as "救我!" still are.
|
|
22
|
+
# Same copy in video-cut, video-script and video-assemble (parity-tested by function).
|
|
23
|
+
_NON_DIALOGUE_TOKENS = frozenset(
|
|
24
|
+
"啊 嗯 哼 哦 呃 唉 嘿 呦 哈 呀 hi yeah ok okay oh uh ah hmm".split()
|
|
25
|
+
)
|
|
26
|
+
_NON_DIALOGUE_CJK = frozenset("啊嗯哼哦呃唉嘿呦哈呀")
|
|
27
|
+
# Lines cross 15s ASR window edges (a line may run 13.2–15.4 while its window ends at 15.0),
|
|
28
|
+
# so an interjection-only window next to real dialogue keeps this much of its shared edge.
|
|
29
|
+
_INTERJECTION_GUARD_SECONDS = 1.0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _interjection_only(text):
|
|
33
|
+
tokens = [token for token in re.split(r"[\W_]+", text.lower()) if token]
|
|
34
|
+
# Punctuation-only rows ("……", "?") are often ASR for unintelligible speech: keep them.
|
|
35
|
+
return bool(tokens) and all(
|
|
36
|
+
token in _NON_DIALOGUE_TOKENS or set(token) <= _NON_DIALOGUE_CJK for token in tokens
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _dialogue_speech_spans(rows):
|
|
41
|
+
"""Merged dialogue spans from timed ASR rows.
|
|
42
|
+
|
|
43
|
+
A row holding only interjections ("啊!", "Hi.") is not dialogue, except a
|
|
44
|
+
`_INTERJECTION_GUARD_SECONDS` guard on an edge it shares with a dialogue row. A row
|
|
45
|
+
whose text is empty or whitespace (ASR heard no words) is timing-only evidence: it is
|
|
46
|
+
skipped here and guards nothing. A row with no `text` field is measured timing whose
|
|
47
|
+
words are unknown and counts as dialogue.
|
|
48
|
+
"""
|
|
49
|
+
rows = sorted(
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
"start": row["start"],
|
|
53
|
+
"end": row["end"],
|
|
54
|
+
"dialogue": not _interjection_only(row.get("text", "")),
|
|
55
|
+
}
|
|
56
|
+
for row in rows
|
|
57
|
+
if "text" not in row or row["text"].strip()
|
|
58
|
+
),
|
|
59
|
+
key=lambda row: (row["start"], row["end"]),
|
|
60
|
+
)
|
|
61
|
+
spans = []
|
|
62
|
+
for idx, row in enumerate(rows):
|
|
63
|
+
if row["dialogue"]:
|
|
64
|
+
spans.append({"start": row["start"], "end": row["end"]})
|
|
65
|
+
continue
|
|
66
|
+
before = rows[idx - 1] if idx > 0 else None
|
|
67
|
+
after = rows[idx + 1] if idx + 1 < len(rows) else None
|
|
68
|
+
if before and before["dialogue"] and row["start"] - before["end"] <= 0.05:
|
|
69
|
+
end = min(row["end"], row["start"] + _INTERJECTION_GUARD_SECONDS)
|
|
70
|
+
spans.append({"start": row["start"], "end": end})
|
|
71
|
+
if after and after["dialogue"] and after["start"] - row["end"] <= 0.05:
|
|
72
|
+
start = max(row["start"], row["end"] - _INTERJECTION_GUARD_SECONDS)
|
|
73
|
+
spans.append({"start": start, "end": row["end"]})
|
|
74
|
+
spans.sort(key=lambda row: (row["start"], row["end"]))
|
|
75
|
+
merged = []
|
|
76
|
+
for span in spans:
|
|
77
|
+
if merged and span["start"] <= merged[-1]["end"] + 0.05:
|
|
78
|
+
merged[-1]["end"] = max(merged[-1]["end"], span["end"])
|
|
79
|
+
else:
|
|
80
|
+
merged.append(span)
|
|
81
|
+
return merged
|
|
82
|
+
|
|
83
|
+
|
|
18
84
|
def _read_json(path):
|
|
19
85
|
"""JSON artifact, or None when the optional file was never written."""
|
|
20
86
|
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else None
|
|
21
87
|
|
|
22
88
|
|
|
89
|
+
def _source_asr_rows(work_dir):
|
|
90
|
+
"""Cleaned ASR segments (asr_clean.json) when present, else raw asr_result.json.
|
|
91
|
+
|
|
92
|
+
Same precedence as video-cut's edge gate and video-assemble's entry check, so one
|
|
93
|
+
transcript decides lint, cut and assemble verdicts. Blank-text rows are timing-only
|
|
94
|
+
evidence (ASR heard no words) and are not source speech.
|
|
95
|
+
"""
|
|
96
|
+
clean = _read_json(work_dir / "asr_clean.json")
|
|
97
|
+
rows = clean["segments"] if clean is not None else _read_json(work_dir / "asr_result.json")
|
|
98
|
+
return [row for row in rows or [] if row["text"].strip()]
|
|
99
|
+
|
|
100
|
+
|
|
23
101
|
def _output_payload_is_current(payload, work_dir):
|
|
24
102
|
plan_path = Path(work_dir) / "clip_plan_validated.json"
|
|
25
103
|
return (
|
|
@@ -38,9 +116,7 @@ def load_source_sentence_evidence(work_dir, mode="full"):
|
|
|
38
116
|
payload = _read_json(work_dir / "speech_boundary_anchors.json") or {
|
|
39
117
|
"sentence_anchors": []
|
|
40
118
|
}
|
|
41
|
-
speech_spans =
|
|
42
|
-
row for row in _read_json(work_dir / "asr_result.json") or [] if row["text"]
|
|
43
|
-
]
|
|
119
|
+
speech_spans = _source_asr_rows(work_dir)
|
|
44
120
|
quiet_windows = [
|
|
45
121
|
row
|
|
46
122
|
for row in _read_json(work_dir / "silence_periods.json") or []
|
|
@@ -54,14 +130,22 @@ def load_source_sentence_evidence(work_dir, mode="full"):
|
|
|
54
130
|
payload = {"sentence_anchors": [], "speech_spans": [], "quiet_windows": []}
|
|
55
131
|
speech_spans = payload["speech_spans"]
|
|
56
132
|
quiet_windows = payload["quiet_windows"]
|
|
133
|
+
# `boundary_use` (schema 2) keeps coarse-ASR estimates usable as `unverified`; schema-1
|
|
134
|
+
# anchors (no `boundary_use`) were all coarse estimates: high/medium are `unverified`.
|
|
57
135
|
anchors = [
|
|
58
136
|
anchor
|
|
59
137
|
for anchor in payload["sentence_anchors"]
|
|
60
|
-
if
|
|
138
|
+
if (
|
|
139
|
+
anchor.get("boundary_use")
|
|
140
|
+
or ("unverified" if anchor["confidence"] in {"high", "medium"} else "none")
|
|
141
|
+
)
|
|
142
|
+
!= "none"
|
|
61
143
|
]
|
|
62
144
|
return {
|
|
63
145
|
"anchors": sorted(anchors, key=lambda item: item["time"]),
|
|
64
146
|
"speech_spans": speech_spans,
|
|
147
|
+
# Entry ownership only: an interjection-only window has no sentence to interrupt.
|
|
148
|
+
"dialogue_spans": _dialogue_speech_spans(speech_spans),
|
|
65
149
|
"quiet_windows": quiet_windows,
|
|
66
150
|
"require_measured": mode == "cut_output",
|
|
67
151
|
}
|
|
@@ -124,7 +208,7 @@ def entry_overlaps_source_speech(start, evidence, *, authored_overlap=True, tole
|
|
|
124
208
|
return False
|
|
125
209
|
if any(
|
|
126
210
|
row["start"] - tolerance <= start < row["end"] - tolerance
|
|
127
|
-
for row in evidence["
|
|
211
|
+
for row in evidence["dialogue_spans"]
|
|
128
212
|
):
|
|
129
213
|
return True
|
|
130
214
|
if evidence["speech_spans"]:
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"""video-script validation entrypoint.
|
|
3
3
|
|
|
4
4
|
Validate an agent-written narration.json against the local understanding index.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
``overlaps_speech`` ownership.
|
|
5
|
+
Validation never rewrites the agent's text, timing, order or metadata. Full-mode text
|
|
6
|
+
that does not fit its window fails lint and goes back to the author. full and cut_output
|
|
7
|
+
persist only the measured ``overlaps_speech`` ownership.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import argparse
|
|
@@ -12,13 +12,9 @@ import json
|
|
|
12
12
|
import math
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
from lib import
|
|
16
|
-
from narration_lint import
|
|
17
|
-
_validate_narration_budget,
|
|
18
|
-
validate_narration_or_raise,
|
|
19
|
-
)
|
|
15
|
+
from lib import log
|
|
16
|
+
from narration_lint import NarrationLintError, validate_narration_or_raise
|
|
20
17
|
from speech_ownership import measure_narration_speech_ownership
|
|
21
|
-
from timeline_fusion import _align_narration_to_quiet
|
|
22
18
|
|
|
23
19
|
|
|
24
20
|
def _load(path):
|
|
@@ -57,16 +53,16 @@ def _validate_output_timeline_bounds(narration, duration, tolerance=0.05):
|
|
|
57
53
|
start, end = seg["start"], seg["end"]
|
|
58
54
|
if end <= -tolerance or start >= duration + tolerance:
|
|
59
55
|
problems.append(
|
|
60
|
-
f"
|
|
56
|
+
f"段 {idx + 1} [{start:.3f},{end:.3f}] fully outside output_duration={duration:.3f}"
|
|
61
57
|
)
|
|
62
58
|
continue
|
|
63
59
|
if start < -tolerance:
|
|
64
60
|
problems.append(
|
|
65
|
-
f"
|
|
61
|
+
f"段 {idx + 1} start={start:.3f} before output timeline (output_duration={duration:.3f})"
|
|
66
62
|
)
|
|
67
63
|
if end > duration + tolerance:
|
|
68
64
|
problems.append(
|
|
69
|
-
f"
|
|
65
|
+
f"段 {idx + 1} end={end:.3f} exceeds output_duration={duration:.3f}"
|
|
70
66
|
)
|
|
71
67
|
if problems:
|
|
72
68
|
raise SystemExit(
|
|
@@ -76,8 +72,17 @@ def _validate_output_timeline_bounds(narration, duration, tolerance=0.05):
|
|
|
76
72
|
|
|
77
73
|
|
|
78
74
|
def main():
|
|
75
|
+
try:
|
|
76
|
+
_validate(_parse_args())
|
|
77
|
+
except NarrationLintError as exc:
|
|
78
|
+
# A lint failure is an ordinary result the author fixes from the summary,
|
|
79
|
+
# not a crash: exit non-zero with the summary only, no traceback.
|
|
80
|
+
raise SystemExit(str(exc)) from None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _parse_args():
|
|
79
84
|
ap = argparse.ArgumentParser(
|
|
80
|
-
description="Validate
|
|
85
|
+
description="Validate agent-written narration.json and measure speech ownership."
|
|
81
86
|
)
|
|
82
87
|
ap.add_argument("--work-dir", required=True)
|
|
83
88
|
ap.add_argument("--mode", default="full", choices=["full", "cut", "cut_output"])
|
|
@@ -87,21 +92,16 @@ def main():
|
|
|
87
92
|
default=None,
|
|
88
93
|
help="cut_output: rendered edited_source.mp4 duration in seconds",
|
|
89
94
|
)
|
|
90
|
-
ap.
|
|
91
|
-
"--preserve-approved-text",
|
|
92
|
-
action="store_true",
|
|
93
|
-
help="validate approved narration without rewriting, truncating, merging, or reordering it",
|
|
94
|
-
)
|
|
95
|
-
args = ap.parse_args()
|
|
95
|
+
return ap.parse_args()
|
|
96
96
|
|
|
97
|
+
|
|
98
|
+
def _validate(args):
|
|
97
99
|
work_dir = Path(args.work_dir)
|
|
98
|
-
CONFIG["edit_mode"] = args.mode
|
|
99
100
|
narration_path = work_dir / "narration.json"
|
|
100
101
|
narration = _load(narration_path)
|
|
101
102
|
if narration is None:
|
|
102
103
|
raise SystemExit(f"缺少 {narration_path};请先按 video-script 规则写解说词")
|
|
103
104
|
vlm_analysis = _load(work_dir / "vlm_analysis.json")
|
|
104
|
-
silence_periods = _load(work_dir / "silence_periods.json") or []
|
|
105
105
|
if args.mode == "cut_output":
|
|
106
106
|
# Two-pass cut: narration is authored in OUTPUT time against edited_source.mp4 — there is
|
|
107
107
|
# no source-time clip membership check. Lint the authored shape first, then derive speech
|
|
@@ -109,7 +109,7 @@ def main():
|
|
|
109
109
|
# voiceover/assemble instead of trusting JSON.
|
|
110
110
|
report = validate_narration_or_raise(
|
|
111
111
|
narration, None, clip_plan=None, mode="cut_output", work_dir=work_dir,
|
|
112
|
-
|
|
112
|
+
output_duration=args.output_duration,
|
|
113
113
|
)
|
|
114
114
|
narration = measure_narration_speech_ownership(
|
|
115
115
|
narration, work_dir, mode="cut_output"
|
|
@@ -139,21 +139,13 @@ def main():
|
|
|
139
139
|
validate_narration_or_raise(
|
|
140
140
|
narration, vlm_analysis, clip_plan=clip_plan, mode="cut", work_dir=work_dir
|
|
141
141
|
)
|
|
142
|
-
if not args.preserve_approved_text:
|
|
143
|
-
narration = _validate_narration_budget(narration, vlm_analysis)
|
|
144
142
|
else:
|
|
143
|
+
# Same contract as cut_output: lint the authored shape, then persist only the
|
|
144
|
+
# measured speech ownership (speech spans minus quiet windows).
|
|
145
145
|
validate_narration_or_raise(
|
|
146
|
-
narration, vlm_analysis, clip_plan=None, mode="full", work_dir=work_dir
|
|
147
|
-
require_chronological=args.preserve_approved_text,
|
|
146
|
+
narration, vlm_analysis, clip_plan=None, mode="full", work_dir=work_dir
|
|
148
147
|
)
|
|
149
|
-
|
|
150
|
-
narration = measure_narration_speech_ownership(
|
|
151
|
-
narration, work_dir, mode="full"
|
|
152
|
-
)
|
|
153
|
-
else:
|
|
154
|
-
narration = _align_narration_to_quiet(
|
|
155
|
-
narration, vlm_analysis, silence_periods
|
|
156
|
-
)
|
|
148
|
+
narration = measure_narration_speech_ownership(narration, work_dir, mode="full")
|
|
157
149
|
narration_path.write_text(
|
|
158
150
|
json.dumps(narration, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
159
151
|
)
|
|
@@ -36,7 +36,7 @@ description: >
|
|
|
36
36
|
export MIMO_API_KEY=***
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
ASR 使用 `mimo-v2.5-asr`;VLM 使用 `mimo-v2.5`。`--skip-asr` 可跳过对白转写,但完整理解仍需要 `MIMO_API_KEY` 运行 VLM。`--mimo-video-overview`
|
|
39
|
+
ASR 使用 `mimo-v2.5-asr`;VLM 使用 `mimo-v2.5`。`--skip-asr` 可跳过对白转写,但完整理解仍需要 `MIMO_API_KEY` 运行 VLM。`--mimo-video-overview` 可开启按场景块的视频概览。未设置 key 时重跑会复用已缓存的转写、画面分析、概览与故事索引(key 决定的默认 endpoint 不参与比对);需要请求模型的 consolidation 记为 `skipped_no_key`,不发请求。缓存对不上(例如复制 work_dir 时没保留文件时间)而已有转写时,运行停下并保留转写:用 `cp -p` / `cp -Rp` / `rsync -t` 保留时间重新复制,或设置 key 后重跑(会重新转写)。不要用 `--skip-asr` 绕过,它会把现有转写替换成 `[]`。
|
|
40
40
|
|
|
41
41
|
若 `work_dir/background_research.json` 存在,本技能会把剧情梗概和角色名折入 VLM 上下文;`--context` 可补充一条简短提示。
|
|
42
42
|
|
|
@@ -45,19 +45,44 @@ ASR 使用 `mimo-v2.5-asr`;VLM 使用 `mimo-v2.5`。`--skip-asr` 可跳过对
|
|
|
45
45
|
## 4. 运行命令
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
python3 scripts/understand.py <video> --work-dir <work_dir>
|
|
49
|
-
[--context "节目名/角色名"] [--scene-threshold 0.1] [--skip-asr] [--mimo-video-overview] [--force]
|
|
48
|
+
python3 scripts/understand.py <video> --work-dir <work_dir> [选项]
|
|
50
49
|
```
|
|
51
50
|
|
|
51
|
+
| 选项 | 默认 | 作用 |
|
|
52
|
+
|------|------|------|
|
|
53
|
+
| `<video>` | 必填 | 源视频 |
|
|
54
|
+
| `--work-dir` | 必填 | 产物目录;不存在时创建 |
|
|
55
|
+
| `--context "..."` | 空 | 补充给 VLM 的简短上下文(节目名、角色名),与 `background_research.json` 合并 |
|
|
56
|
+
| `--scene-threshold` | `0.1` | 场景检测阈值 |
|
|
57
|
+
| `--style` | `纪录片` | 写进创作简报的解说风格 |
|
|
58
|
+
| `--edit-mode full\|cut` | 不设 | 写进简报的 recap 模式;`cut` 时按剪后时长估算旁白预算,已有 `edited_source.mp4` 时句末锚点改用剪后时间 |
|
|
59
|
+
| `--target-duration` | 不设 | 写进简报的 cut 目标时长;尚无 `clip_plan_validated.json` 时用它估算旁白预算 |
|
|
60
|
+
| `--skip-asr` | 关 | 不转写对白,把 `asr_result.json` 写成 `[]`(已有转写会被覆盖),ASR 证据标为显式跳过 |
|
|
61
|
+
| `--mimo-video-overview` | 关 | 按场景块运行 MiMo 视频概览,并作为逐场景主描述 |
|
|
62
|
+
| `--force` | 关 | 忽略缓存,全部重算 |
|
|
63
|
+
| `--brief-only` | 关 | 只用现有产物重建 `agent_narration_brief.md`,不抽帧、不调 API |
|
|
64
|
+
| `--edited-storyboard-only` | 关 | 只按 `clip_plan_validated.json` 写剪后时间线 `storyboard/edited_storyboard.*`,并在已有的 `agent_narration_brief.md` 顶部加 storyboard 指引;多源计划(带 `sources`)从各来源 `source_work_dir` 的 `frames/` 按其 `frames_manifest.json` 的 fps 取帧,tile 标 `S1`/`S2`…;不抽帧、不调 API,故事板生成失败只记日志。与 `--brief-only` 互斥 |
|
|
65
|
+
| `--consolidate` / `--no-consolidate` | 开 | 生成全局故事索引 `understanding_index.*` |
|
|
66
|
+
| `--consolidate-asr` | 关 | 另外清洗 ASR 文本,写 `asr_clean.json` |
|
|
67
|
+
|
|
52
68
|
## 5. 输出契约
|
|
53
69
|
|
|
70
|
+
默认运行写出下表产物;各阶段产物旁的 `*.meta.json` 是缓存 provenance sidecar。
|
|
71
|
+
|
|
54
72
|
| 文件 | 内容 |
|
|
55
73
|
|------|------|
|
|
74
|
+
| `frames/frame_*.jpg`、`frames_manifest.json` | 按 fps 抽出的帧及其清单 |
|
|
56
75
|
| `scenes.json` | 场景切点、起止时间与时长 |
|
|
76
|
+
| `audio.wav` | 16 kHz 单声道音频,供 ASR、静音检测与句末锚点使用 |
|
|
57
77
|
| `asr_result.json` | `[{start, end, text}]` 时间戳对白 |
|
|
58
78
|
| `asr_timing_evidence.json` | ASR 可用性状态、粗窗口精度、glossary 前后文本,以及它所描述的源视频/音频/结果文件(路径存在性 + size/mtime) |
|
|
59
|
-
| `vlm_analysis.json` | 逐场景描述、深层分析与 `frame_facts` |
|
|
60
79
|
| `silence_periods.json` | `[{start, end, duration, has_speech}]` 安静窗口 |
|
|
80
|
+
| `speech_boundary_anchors.json` | ASR 句末标点对齐到短停顿的句末锚点;缺音频或 ASR 时 `status: unavailable` |
|
|
81
|
+
| `vlm_analysis.json` | 逐场景描述、深层分析与 `frame_facts` |
|
|
82
|
+
| `mimo_video_overview.status.json` | 视频概览状态(未启用时为 `disabled`);启用成功另写 `mimo_video_overview.json` |
|
|
83
|
+
| `understanding_index.json`、`understanding_index.md` | 全局故事索引(`--no-consolidate` 时不写) |
|
|
84
|
+
| `consolidation.status.json` | 故事索引与 ASR 清洗的运行状态 |
|
|
85
|
+
| `storyboard/source_storyboard.{json,jpg}` | 原片时间线联系表(超页时续写 `_001.jpg` 等);已有 `clip_plan_validated.json` 时另写 `edited_storyboard.*`;`STORYBOARD=0` 关闭 |
|
|
61
86
|
| `timeline_fusion.json` | VLM、ASR 与静音信息的统一时间线 |
|
|
62
87
|
| `asr_writing_chunks.json` | 按句界和场景切分的 ASR 写作块 |
|
|
63
88
|
| `agent_narration_brief.md` | Agent 首先阅读的创作简报 |
|
|
@@ -79,11 +79,9 @@ ASR 的独立证据 sidecar,不改变 `asr_result.json` 的既有数组结构
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
`status` 可为 `AVAILABLE_COARSE`、`EXPLICITLY_SKIPPED`、`UNAVAILABLE_NO_KEY`、
|
|
82
|
-
`UNAVAILABLE_NO_DURATION`、`FAILED_AUDIO_EXTRACTION`、`FAILED_PROVIDER
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
当时参与修正的人名/别名列表(`glossary.names`),人名表变化或所描述的文件被重写(size/mtime 不再
|
|
86
|
-
一致)都会使 ASR 缓存失效。
|
|
82
|
+
`UNAVAILABLE_NO_DURATION`、`FAILED_AUDIO_EXTRACTION`、`FAILED_PROVIDER` 或 `EMPTY_UNKNOWN`。
|
|
83
|
+
sidecar 记录当时参与修正的人名/别名列表(`glossary.names`),人名表变化、所描述的文件被重写
|
|
84
|
+
(size/mtime 不再一致)或 sidecar 缺失,都会使 ASR 缓存失效并重跑 ASR。
|
|
87
85
|
`UNAVAILABLE_NO_DURATION` 与 `EMPTY_UNKNOWN` 是可重试的不可用结果,不作为缓存命中;写作
|
|
88
86
|
brief 会校验 sidecar 并打印当前状态,缺失或与当前文件不一致时显示 `MISSING_OR_STALE`。
|
|
89
87
|
|
|
@@ -143,22 +141,7 @@ brief 会校验 sidecar 并打印当前状态,缺失或与当前文件不一
|
|
|
143
141
|
]
|
|
144
142
|
```
|
|
145
143
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`deslop_qc_requirements.json` 是 tool/brief generated run contract:工具或 brief 生成本次运行的 QC 要求,供 `deslop_qc` 读取,不由 Agent 手写。字段为 `schema_version` 与 `style_card_required`。
|
|
149
|
-
|
|
150
|
-
`style_card_required` 默认 `false`(advisory):缺少 `style_card.json` 只是 warning,不阻断出片。将来的 opt-in 运行可把它设为 `true`,让 `style_card.json` 成为硬性要求——`deslop_qc` 只读这个字段判断缺少 `style_card.json` 是否是 blocker,不扫描 `agent_narration_brief.md` 的 prompt wording 来推断。如果 requirements 文件缺失或损坏,按 legacy/migration advisory 处理,不作为 hard failure。
|
|
151
|
-
|
|
152
|
-
该契约不改变 `--style`:`--style` 仍是 freeform verbatim guidance,不增加固定风格档位。它也不改变 `deslop_qc` 边界:仍然是 report-only,不是 AIGC detector,不自动改写。
|
|
153
|
-
|
|
154
|
-
最小示例:
|
|
155
|
-
|
|
156
|
-
```json
|
|
157
|
-
{
|
|
158
|
-
"schema_version": 1,
|
|
159
|
-
"style_card_required": false
|
|
160
|
-
}
|
|
161
|
-
```
|
|
144
|
+
cut 第二轮(已有 `edited_source.mp4`)时本文件与 `asr_writing_chunks.json` 都在 OUTPUT 时间轴上:被拆到多个片段的场景 `scene_id` 写作 `"3.1"`(0 起的原场景号加片段序号);VLM 文字里落在场景原片区间内的时间改写成输出时间,落在被剪掉部分的写 `[cut-away moment]`;只被剪进一部分的 ASR 窗口文字换成 `[partial ASR window: only part of it is in the cut, text withheld]`。`scene_id` 一律 0 起,brief 里给人看的场景号从 1 数。
|
|
162
145
|
|
|
163
146
|
## background_research.json
|
|
164
147
|
|
|
@@ -190,4 +173,4 @@ brief 会校验 sidecar 并打印当前状态,缺失或与当前文件不一
|
|
|
190
173
|
|
|
191
174
|
## 其他产物
|
|
192
175
|
|
|
193
|
-
`narration.json`、`narration_lint.json`、`style_card.json`、`packaging_plan.json`、`
|
|
176
|
+
`narration.json`、`narration_lint.json`、`style_card.json`、`packaging_plan.json`、`clip_plan.json`、`clip_plan_validated.json` 由后续的写稿与剪辑阶段读写,本技能既不生成也不校验它们;其格式以本技能生成的 `agent_narration_brief.md` 和编排器的中间产物契约为准。
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Split, chunk, and budget text for the agent narration brief."""
|
|
2
2
|
|
|
3
|
-
import copy
|
|
4
3
|
import re
|
|
5
4
|
|
|
6
|
-
from lib import CONFIG
|
|
7
|
-
|
|
5
|
+
from lib import CONFIG
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _sentence_pieces(text: str) -> list[str]:
|
|
9
|
+
"""Split text into sentence-like pieces while keeping terminal punctuation."""
|
|
10
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
11
|
+
if not text:
|
|
12
|
+
return []
|
|
13
|
+
parts = re.split(r"([。!?!?;;.])", text)
|
|
14
|
+
out: list[str] = []
|
|
15
|
+
for idx in range(0, len(parts), 2):
|
|
16
|
+
body = parts[idx].strip()
|
|
17
|
+
punct = parts[idx + 1] if idx + 1 < len(parts) else ""
|
|
18
|
+
if body or punct:
|
|
19
|
+
out.append((body + punct).strip())
|
|
20
|
+
return out or [text]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _text_units(text: str) -> int:
|
|
24
|
+
"""Length unit for prose: CJK characters, otherwise words."""
|
|
25
|
+
if re.search(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]", text):
|
|
26
|
+
return len(re.sub(r"\s+", "", text))
|
|
27
|
+
return len(re.findall(r"\b\w+\b", text))
|
|
8
28
|
|
|
9
29
|
|
|
10
30
|
def _format_frame_facts(scene):
|
|
@@ -16,17 +36,6 @@ def _format_frame_facts(scene):
|
|
|
16
36
|
return "\n 帧动作:\n" + "\n".join(lines)
|
|
17
37
|
|
|
18
38
|
|
|
19
|
-
def _text_char_count(text):
|
|
20
|
-
"""计算文本的有效字数(去除标点和空白,这些不占 TTS 朗读时间)。"""
|
|
21
|
-
return len(
|
|
22
|
-
re.sub(
|
|
23
|
-
r'[,。!?、;:…“”‘’《》〈〉\s"\'「」『』()()【】\[\]—~·,.!?;:\\-]',
|
|
24
|
-
"",
|
|
25
|
-
text,
|
|
26
|
-
)
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
39
|
def _split_text_by_sentence_windows(text, min_chars=500, max_chars=800):
|
|
31
40
|
"""Clipto-style three-tier sentence boundary splitting for long ASR text."""
|
|
32
41
|
text = re.sub(r"\s+", " ", text).strip()
|
|
@@ -177,66 +186,6 @@ def _chunk_asr_for_writing(segments, scenes_analysis, min_chars=None, max_chars=
|
|
|
177
186
|
return chunks
|
|
178
187
|
|
|
179
188
|
|
|
180
|
-
def _truncate_at_sentence(text, max_chars):
|
|
181
|
-
"""在句子边界截断,不产生残句。max_chars 按有效字符计(不含标点空白)。"""
|
|
182
|
-
if _text_char_count(text) <= max_chars:
|
|
183
|
-
return text
|
|
184
|
-
eff = 0
|
|
185
|
-
cutoff = len(text)
|
|
186
|
-
for i, ch in enumerate(text):
|
|
187
|
-
eff += 1 if _text_char_count(ch) else 0
|
|
188
|
-
if eff > max_chars:
|
|
189
|
-
cutoff = i + 1
|
|
190
|
-
break
|
|
191
|
-
idx = max(text[:cutoff].rfind(sep) for sep in ["。", "!", "?", "!", "?"])
|
|
192
|
-
if idx > 0:
|
|
193
|
-
return text[: idx + 1]
|
|
194
|
-
idx = max(text[:cutoff].rfind(sep) for sep in [",", "、", ";", ","])
|
|
195
|
-
if idx > 3:
|
|
196
|
-
return text[:idx] + "。"
|
|
197
|
-
return ""
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _char_bigrams(text):
|
|
201
|
-
return {text[i : i + 2] for i in range(len(text) - 1) if text[i : i + 2].strip()}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _post_dedup_narration(narration):
|
|
205
|
-
"""去除相邻相似解说段(bigram 重叠 >60% 则合并)。"""
|
|
206
|
-
if len(narration) < 2:
|
|
207
|
-
return narration
|
|
208
|
-
result = [narration[0]]
|
|
209
|
-
for seg in narration[1:]:
|
|
210
|
-
prev = result[-1]
|
|
211
|
-
set_a, set_b = _char_bigrams(prev["narration"]), _char_bigrams(seg["narration"])
|
|
212
|
-
if not set_a or not set_b:
|
|
213
|
-
result.append(seg)
|
|
214
|
-
continue
|
|
215
|
-
overlap = len(set_a & set_b) / min(len(set_a), len(set_b))
|
|
216
|
-
# Only merge near-identical adjacent beats. Short Chinese beats share many
|
|
217
|
-
# bigrams by chance, so a low threshold collapses intentional parallel beats
|
|
218
|
-
# ("他不再试探" / "他直接赌上全力") and silently drops density below target.
|
|
219
|
-
if overlap > 0.6:
|
|
220
|
-
# Validation is also the handoff boundary for renderer metadata: keep the
|
|
221
|
-
# overlays authored on either merged beat.
|
|
222
|
-
merged_overlays = copy.deepcopy(
|
|
223
|
-
prev.get("visual_overlays", []) + seg.get("visual_overlays", [])
|
|
224
|
-
)
|
|
225
|
-
if len(seg["narration"]) > len(prev["narration"]):
|
|
226
|
-
prev["narration"] = seg["narration"]
|
|
227
|
-
prev["end"] = seg["end"]
|
|
228
|
-
prev["pause_after_ms"] = seg["pause_after_ms"]
|
|
229
|
-
if merged_overlays:
|
|
230
|
-
prev["visual_overlays"] = merged_overlays
|
|
231
|
-
log(f" 去重合并: {prev['start']:.0f}-{prev['end']:.0f}s")
|
|
232
|
-
else:
|
|
233
|
-
result.append(seg)
|
|
234
|
-
removed = len(narration) - len(result)
|
|
235
|
-
if removed:
|
|
236
|
-
log(f" 去重: {len(narration)} → {len(result)} 段 (合并 {removed} 段)")
|
|
237
|
-
return result
|
|
238
|
-
|
|
239
|
-
|
|
240
189
|
def _scene_available_seconds(start, end):
|
|
241
190
|
return max(0.0, end - start - CONFIG["narration_tail_pad_seconds"])
|
|
242
191
|
|
|
@@ -251,51 +200,5 @@ def _recommended_char_budget(start, end):
|
|
|
251
200
|
return int(_scene_available_seconds(start, end) * effective_rate)
|
|
252
201
|
|
|
253
202
|
|
|
254
|
-
def _find_scene_for_midpoint(scenes_analysis, start, end):
|
|
255
|
-
mid = (start + end) / 2
|
|
256
|
-
for scene in scenes_analysis:
|
|
257
|
-
if scene["start"] <= mid <= scene["end"]:
|
|
258
|
-
return scene
|
|
259
|
-
return None
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def _normalise_narration_segment(seg):
|
|
263
|
-
"""Normalize one lint-validated narration segment for the full-mode rewrite.
|
|
264
|
-
|
|
265
|
-
Authored timing is a delivery contract: scene boundaries are approximate
|
|
266
|
-
visual-analysis buckets, so start/end are never clamped here.
|
|
267
|
-
"""
|
|
268
|
-
item = {
|
|
269
|
-
"start": round(seg["start"], 2),
|
|
270
|
-
"end": round(seg["end"], 2),
|
|
271
|
-
"narration": str(seg["narration"]).strip(),
|
|
272
|
-
"pause_after_ms": int(seg.get("pause_after_ms", CONFIG["breath_ms"])),
|
|
273
|
-
"overlaps_speech": bool(seg.get("overlaps_speech", True)),
|
|
274
|
-
}
|
|
275
|
-
for optional_key in (
|
|
276
|
-
"source_start",
|
|
277
|
-
"source_end",
|
|
278
|
-
"source_clip_id",
|
|
279
|
-
"source_entry_policy",
|
|
280
|
-
"source_entry_reason",
|
|
281
|
-
):
|
|
282
|
-
if optional_key in seg:
|
|
283
|
-
item[optional_key] = seg[optional_key]
|
|
284
|
-
# carry the per-beat emotion/tone tag (MiMo TTS instruct) through lint untouched
|
|
285
|
-
if seg.get("emotion"):
|
|
286
|
-
item["emotion"] = seg["emotion"].strip()
|
|
287
|
-
# Renderer-owned metadata survives the validation rewrite; the recap orchestrator
|
|
288
|
-
# filters supported overlay kinds later.
|
|
289
|
-
if "visual_overlays" in seg:
|
|
290
|
-
item["visual_overlays"] = copy.deepcopy(seg["visual_overlays"])
|
|
291
|
-
return item
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def _clean_narration_punctuation(text):
|
|
295
|
-
text = re.sub(r"\s+", " ", text).strip()
|
|
296
|
-
text = re.sub(r'[,:、;,]["\']?[。!?]', "。", text)
|
|
297
|
-
return re.sub(r'["\']。$', "。", text)
|
|
298
|
-
|
|
299
|
-
|
|
300
203
|
def _overlap_seconds(start, end, other_start, other_end):
|
|
301
204
|
return max(0.0, min(end, other_end) - max(start, other_start))
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from lib import CONFIG
|
|
9
|
-
from lib import log, run_cmd, get_video_duration, mimo_asr_api_call
|
|
9
|
+
from lib import log, run_cmd, get_video_duration, mimo_asr_api_call, is_moderation_refusal
|
|
10
10
|
from detect import _audio_meta_path, _write_audio_meta
|
|
11
11
|
from asr_timing_evidence import (
|
|
12
12
|
EVIDENCE_FILENAME,
|
|
@@ -202,13 +202,13 @@ def _run_asr(wav_path):
|
|
|
202
202
|
|
|
203
203
|
音频以 base64 data-URI 放进 OpenAI 风格的 chat/completions 消息里,转写文本回到
|
|
204
204
|
choices[0].message.content。API/响应结构失败会抛错,避免把瞬时失败缓存成空转写;
|
|
205
|
-
|
|
205
|
+
音频文件缺失/不可读同样抛错(那是上游 bug,不是静音);只有空音频、超体积等
|
|
206
|
+
确定不可发送的片段返回空串。
|
|
206
207
|
"""
|
|
207
208
|
try:
|
|
208
209
|
raw = Path(wav_path).read_bytes()
|
|
209
210
|
except OSError as e:
|
|
210
|
-
|
|
211
|
-
return ""
|
|
211
|
+
raise RuntimeError(f"ASR: 无法读取音频 {wav_path}: {e}") from e
|
|
212
212
|
if not raw:
|
|
213
213
|
return ""
|
|
214
214
|
|
|
@@ -235,11 +235,17 @@ def _run_asr(wav_path):
|
|
|
235
235
|
except Exception as e:
|
|
236
236
|
raise ASRProviderError(f"MiMo ASR 调用失败: {e}") from e
|
|
237
237
|
try:
|
|
238
|
-
|
|
238
|
+
text = _strip_reasoning_residue(str(resp["choices"][0]["message"]["content"] or "")).strip()
|
|
239
239
|
except (KeyError, IndexError, TypeError):
|
|
240
240
|
raise ASRProviderError(
|
|
241
241
|
f"MiMo ASR 返回结构异常: {json.dumps(resp, ensure_ascii=False)[:200]}"
|
|
242
242
|
)
|
|
243
|
+
if is_moderation_refusal(text):
|
|
244
|
+
# The refusal sentence is not dialogue: storing it would put it in the briefs and
|
|
245
|
+
# make cut/narration safety treat the window as speech. Record no text instead.
|
|
246
|
+
log(f"ASR 警告: MiMo 内容审核拒绝转写 {Path(wav_path).name},该段记为无文本")
|
|
247
|
+
return ""
|
|
248
|
+
return text
|
|
243
249
|
|
|
244
250
|
|
|
245
251
|
def _segment_and_transcribe(audio_wav, segments_dir, total_duration, segment_length=None):
|