@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
|
@@ -23,7 +23,7 @@ description: >
|
|
|
23
23
|
4. 声音/旁白编辑
|
|
24
24
|
5. 第一次观看的观众
|
|
25
25
|
|
|
26
|
-
Agent 先记录简洁决定,再写时间线产物。`validate.py`
|
|
26
|
+
Agent 先记录简洁决定,再写时间线产物。`validate.py` 负责对理解索引做机械校验,从不改写 Agent 的稿子:段落顺序、数量、时间、文本、停顿和扩展元数据原样保留,只依据现有声音证据回写实测的 `overlaps_speech`。文本装不下时间窗、段落未按时间排序等问题以 error 退回给 Agent 修改。
|
|
27
27
|
|
|
28
28
|
下面的 `scripts/...` 均相对于本技能目录。若执行器从仓库根目录启动,请给脚本路径加上本技能的绝对目录。
|
|
29
29
|
|
|
@@ -44,8 +44,9 @@ REVISION 先明确本轮修改项与冻结项,再编辑对应层:表达、
|
|
|
44
44
|
- `work_dir/agent_narration_brief.md`:场景、时长、安静窗口与字数预算。
|
|
45
45
|
- `asr_writing_chunks.json`:长对白的写作分块。
|
|
46
46
|
- `timeline_fusion.json`:判断某段是否有对白或静音槽。
|
|
47
|
-
- `vlm_analysis.json` / `asr_result.json
|
|
47
|
+
- `vlm_analysis.json` / `asr_result.json`:核对具体画面与原声证据;有 `asr_clean.json` 时以它的文本为准(lint、评审、剪辑和合成都读它)。
|
|
48
48
|
- brief 顶部列出的 contact sheet:不要只依赖场景摘要;反应、走位、静止和台词前后的具体时刻常常更重要。
|
|
49
|
+
- `production_reference.json`(仅当 `work_dir` 里有):另一部成片拆出的可迁移方法与节奏数值,用法见 §3。
|
|
49
50
|
|
|
50
51
|
full 模式使用原片时间。cut 模式第一阶段只写 `clip_plan.json`;`edited_source.mp4` 产生后,第二阶段才按输出时间写 `narration.json`。
|
|
51
52
|
|
|
@@ -68,6 +69,8 @@ full 模式使用原片时间。cut 模式第一阶段只写 `clip_plan.json`;
|
|
|
68
69
|
|
|
69
70
|
只记录决定、证据锚点、被放弃的备选方案和简短理由,不写冗长思维过程。
|
|
70
71
|
|
|
72
|
+
若 `work_dir` 有 `production_reference.json`,制定方案前先读它。它来自另一部成片,只含可迁移的方法和测得的节奏,不含本片事实,不能作为本片画面、剧情或台词的证据。优先级:用户指令 > 本片证据 > 参考。CREATE 可把它的 `structure` 当作一个候选假设,与素材自生的假设比较;DIRECTED / REVISION 默认不套用。`targets` 是参考值,不是配额:与本片的 `audio_owner`、完整台词或表演冲突时以素材为准。可在 `recap_story_plan.json` 写可选字段 `reference_methods: [{"id": "m1", "decision": "adopt|adapt|skip", "note": "…"}]`。没有这个文件就跳过本段。
|
|
73
|
+
|
|
71
74
|
### 3.1 导演判断
|
|
72
75
|
|
|
73
76
|
锁定:
|
|
@@ -153,7 +156,7 @@ full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.m
|
|
|
153
156
|
|
|
154
157
|
| 字段 | 含义 |
|
|
155
158
|
|------|------|
|
|
156
|
-
| `start` / `end` | full 模式为原片时间;cut
|
|
159
|
+
| `start` / `end` | full 模式为原片时间;cut 第二阶段为输出时间。与上一块间隔不超过 1.6 秒的块属于同一段落,assemble 会让它紧接上一块的实际结尾(间隔 0.35 秒)播放,最多比 `start` 提前 1.2 秒,且提前的那一段不会进入原声对白;段落首块从 `start` 开始,上一块超时则顺延到上一块结尾加停顿之后 |
|
|
157
160
|
| `narration` | 解说文本 |
|
|
158
161
|
| `pause_after_ms` | 段后停顿,默认 250ms |
|
|
159
162
|
| `overlaps_speech` | 是否与原对白重叠;连续铺底窗口通常为 `true`,真正静音槽才为 `false` |
|
|
@@ -165,7 +168,7 @@ full 模式直接按原片时间写;cut 第二阶段先查看 `edited_source.m
|
|
|
165
168
|
2. **按连续思路写**:旁白拥有一个 beat 时,用一个或少量完整句子完成“前提 → 触发动作 → 变化/意义”,并在一次 TTS 中合成。句号服从口语思路和呼吸,不服从字幕换行;不要固定句数,也不要“一句一停”。
|
|
166
169
|
3. **7:3 不是配额**:只在素材判断不足时作为避免墙到墙旁白的粗略首稿参考。实际比例服从 `audio_owner`;强对白、动作声或沉默可以完整拥有一个 beat。
|
|
167
170
|
4. **视听接力**:旁白若引出原声,块尾要让观众想听;原声结束后的下一块要承接它造成的变化。
|
|
168
|
-
5. **按有效语速控量**:用 `字数 / brief 头部 speech budget`
|
|
171
|
+
5. **按有效语速控量**:用 `字数 / brief 头部 speech budget` 估算朗读秒数,每块再加约 0.45 秒 TTS 首尾静音。brief 里每个窗口标的字数没扣这段静音,2–3 秒的短窗口要比它少写一两个字;装不下时删减或拆分叙事任务,不用加速堆字。
|
|
169
172
|
6. **不看图说话**:旁白只增加上下文、因果、预期、证据支持的解释或跨越。
|
|
170
173
|
7. **人物与证据优先**:优先使用已知角色名;关系、动机、潜台词和结果必须指向 visual / ASR / research / user context,且不能把背景资料伪装成当前画面事实。
|
|
171
174
|
8. **写给耳朵听**:使用具体名词和动词,句子完整、口语可听;避免字幕腔、半句、空泛拔高和破折号。TTS 文本先保证听感连续,字幕再按阅读宽度拆分,不能反过来把朗读稿切碎。
|
|
@@ -218,7 +221,7 @@ python3 scripts/review.py --work-dir <work_dir>
|
|
|
218
221
|
|
|
219
222
|
重复修改并评审,直到:
|
|
220
223
|
|
|
221
|
-
- `verdict` 为 `PASS`
|
|
224
|
+
- `verdict` 为 `PASS` 且没有 `error`;或
|
|
222
225
|
- 对仍保留的问题做明确 override。
|
|
223
226
|
|
|
224
227
|
覆盖决定追加到 `work_dir/narration_review_override.md`:
|
|
@@ -237,11 +240,10 @@ python3 scripts/review.py --work-dir <work_dir>
|
|
|
237
240
|
|
|
238
241
|
```bash
|
|
239
242
|
python3 scripts/validate.py --work-dir <work_dir> --mode full
|
|
240
|
-
python3 scripts/validate.py --work-dir <work_dir> --mode full --preserve-approved-text
|
|
241
243
|
# cut 输出时间线由编排器使用 --mode cut_output
|
|
242
244
|
```
|
|
243
245
|
|
|
244
|
-
命令写出 `narration_lint.json`。full
|
|
246
|
+
命令写出 `narration_lint.json`。full 与 cut_output 用同一套声音归属算法(原声对白区间减去安静窗口)回写 `overlaps_speech`,其余字段原样保留,不截短、不合并、不补标点、不重排。推荐字数按时间窗先扣 0.45 秒 TTS 首尾静音再计算;full 模式下某段字数超过推荐字数的 1.25 倍即报 `over_budget` error,报告里写明时间窗、`budget_chars`、`limit_chars`、`actual_chars` 和 `over_chars`:缩短文字或放宽/挪动时间窗,不要指望 TTS 替你缩稿。有 error 时命令以非零退出,逐块列出「段 N」(narration.json 里第 N 块,从 1 数;`narration_lint.json` 的 `index` 仍从 0 数)、错误码、关键数字和改法。修复所有 error 后重复运行,直到校验干净,再继续 TTS 与合成。
|
|
245
247
|
|
|
246
248
|
片名或题材明确但缺少剧情上下文时,先按本技能的 `references/research-guide.md` 写 `background_research.json`。若理解素材偏薄,brief 中的数量只能当上限:宁可少写、写实,也不要为凑数复述画面。
|
|
247
249
|
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Count and budget agent-written narration text for lint."""
|
|
2
2
|
|
|
3
|
-
import copy
|
|
4
3
|
import re
|
|
5
4
|
|
|
6
|
-
from lib import CONFIG
|
|
7
|
-
from deslop_qc import _sentence_pieces, _text_units
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _format_frame_facts(scene):
|
|
11
|
-
"""将帧动作描述格式化为可注入 agent brief 的文本。"""
|
|
12
|
-
facts = scene.get("frame_facts", {})
|
|
13
|
-
if not facts:
|
|
14
|
-
return ""
|
|
15
|
-
lines = [f" {ts}s: {'; '.join(facts[ts])}" for ts in sorted(facts, key=float)]
|
|
16
|
-
return "\n 帧动作:\n" + "\n".join(lines)
|
|
5
|
+
from lib import CONFIG
|
|
17
6
|
|
|
18
7
|
|
|
19
8
|
def _text_char_count(text):
|
|
@@ -27,214 +16,12 @@ def _text_char_count(text):
|
|
|
27
16
|
)
|
|
28
17
|
|
|
29
18
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return _sentence_pieces(text)
|
|
37
|
-
|
|
38
|
-
result = []
|
|
39
|
-
rest = text
|
|
40
|
-
sentence_marks = "。!?!?;;."
|
|
41
|
-
while _text_units(rest) > max_chars:
|
|
42
|
-
# The min/max thresholds are unit-based but punctuation is char-indexed.
|
|
43
|
-
# For Chinese (the dominant recap target) these are nearly identical; for
|
|
44
|
-
# non-CJK this remains a safe sentence-boundary heuristic around words.
|
|
45
|
-
char_min = min(len(rest), max(1, min_chars))
|
|
46
|
-
char_max = min(len(rest), max(1, max_chars))
|
|
47
|
-
window = rest[:char_max]
|
|
48
|
-
cut = max(window.rfind(mark) for mark in sentence_marks)
|
|
49
|
-
if cut + 1 < char_min:
|
|
50
|
-
outside = -1
|
|
51
|
-
for i, ch in enumerate(rest[char_max:], start=char_max):
|
|
52
|
-
if ch in sentence_marks:
|
|
53
|
-
outside = i
|
|
54
|
-
break
|
|
55
|
-
cut = outside if outside >= 0 and outside + 1 <= len(rest) else char_max - 1
|
|
56
|
-
piece = rest[: cut + 1].strip()
|
|
57
|
-
if not piece:
|
|
58
|
-
piece = rest[:char_max].strip()
|
|
59
|
-
cut = char_max - 1
|
|
60
|
-
result.append(piece)
|
|
61
|
-
rest = rest[cut + 1 :].strip()
|
|
62
|
-
if rest:
|
|
63
|
-
result.append(rest)
|
|
64
|
-
return result
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _timed_sentence_pieces(seg, min_chars, max_chars):
|
|
68
|
-
"""Split one ASR segment into timed sentence pieces with approximate spans."""
|
|
69
|
-
text = seg["text"].strip()
|
|
70
|
-
if not text:
|
|
71
|
-
return []
|
|
72
|
-
start, end = seg["start"], seg["end"]
|
|
73
|
-
pieces = []
|
|
74
|
-
for sentence in _sentence_pieces(text):
|
|
75
|
-
if _text_units(sentence) > max_chars:
|
|
76
|
-
pieces.extend(
|
|
77
|
-
_split_text_by_sentence_windows(
|
|
78
|
-
sentence, min_chars=min_chars, max_chars=max_chars
|
|
79
|
-
)
|
|
80
|
-
)
|
|
81
|
-
else:
|
|
82
|
-
pieces.append(sentence)
|
|
83
|
-
total_units = sum(max(1, _text_units(piece)) for piece in pieces)
|
|
84
|
-
duration = end - start
|
|
85
|
-
cursor = start
|
|
86
|
-
timed = []
|
|
87
|
-
for idx, piece in enumerate(pieces):
|
|
88
|
-
units = max(1, _text_units(piece))
|
|
89
|
-
piece_end = end if idx == len(pieces) - 1 else cursor + duration * units / total_units
|
|
90
|
-
timed.append(
|
|
91
|
-
{
|
|
92
|
-
"start": round(cursor, 2),
|
|
93
|
-
"end": round(piece_end, 2),
|
|
94
|
-
"text": piece,
|
|
95
|
-
"char_count": _text_units(piece),
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
cursor = piece_end
|
|
99
|
-
return timed
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _scene_ids_for_range(scenes, start, end):
|
|
103
|
-
duration = max(0.001, end - start)
|
|
104
|
-
scene_ids = []
|
|
105
|
-
for scene in scenes:
|
|
106
|
-
overlap = _overlap_seconds(start, end, scene["start"], scene["end"])
|
|
107
|
-
# Ignore tiny boundary tails from approximate ASR sentence timing. A
|
|
108
|
-
# scene id should mean the chunk materially belongs to that scene.
|
|
109
|
-
if overlap and (overlap >= duration * 0.2 or overlap >= 3.0):
|
|
110
|
-
scene_ids.append(scene["scene_id"])
|
|
111
|
-
return scene_ids
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _chunk_asr_for_writing(segments, scenes_analysis, min_chars=None, max_chars=None):
|
|
115
|
-
"""Chunk ASR into semantic windows before an agent writes long-dialogue recaps.
|
|
116
|
-
|
|
117
|
-
The strategy mirrors Clipto's segment splitter: accumulate a window, prefer
|
|
118
|
-
the last sentence boundary inside max length, allow a slightly longer first
|
|
119
|
-
boundary outside the window, and fall back to the remaining text. CJK text is
|
|
120
|
-
measured by characters; non-CJK text is measured by words.
|
|
121
|
-
"""
|
|
122
|
-
min_chars = CONFIG["asr_chunk_min_chars"] if min_chars is None else min_chars
|
|
123
|
-
max_chars = CONFIG["asr_chunk_max_chars"] if max_chars is None else max_chars
|
|
124
|
-
pieces = [
|
|
125
|
-
piece
|
|
126
|
-
for seg in segments
|
|
127
|
-
for piece in _timed_sentence_pieces(seg, min_chars, max_chars)
|
|
128
|
-
]
|
|
129
|
-
|
|
130
|
-
chunks = []
|
|
131
|
-
current = []
|
|
132
|
-
current_units = 0
|
|
133
|
-
current_scene_ids = set()
|
|
134
|
-
|
|
135
|
-
def flush():
|
|
136
|
-
nonlocal current, current_units, current_scene_ids
|
|
137
|
-
if not current:
|
|
138
|
-
return
|
|
139
|
-
chunks.append(
|
|
140
|
-
{
|
|
141
|
-
"chunk_id": len(chunks),
|
|
142
|
-
"start": current[0]["start"],
|
|
143
|
-
"end": current[-1]["end"],
|
|
144
|
-
"scene_ids": sorted(
|
|
145
|
-
current_scene_ids, key=lambda sid: (isinstance(sid, str), sid)
|
|
146
|
-
),
|
|
147
|
-
"char_count": current_units,
|
|
148
|
-
"text": " ".join(piece["text"] for piece in current).strip(),
|
|
149
|
-
"segments": current,
|
|
150
|
-
}
|
|
151
|
-
)
|
|
152
|
-
current = []
|
|
153
|
-
current_units = 0
|
|
154
|
-
current_scene_ids = set()
|
|
155
|
-
|
|
156
|
-
for piece in pieces:
|
|
157
|
-
units = max(1, piece["char_count"])
|
|
158
|
-
piece_scene_ids = set(
|
|
159
|
-
_scene_ids_for_range(scenes_analysis, piece["start"], piece["end"])
|
|
160
|
-
)
|
|
161
|
-
crosses_scene = (
|
|
162
|
-
current
|
|
163
|
-
and current_scene_ids
|
|
164
|
-
and piece_scene_ids
|
|
165
|
-
and not (current_scene_ids & piece_scene_ids)
|
|
166
|
-
)
|
|
167
|
-
if crosses_scene and current_units >= min_chars:
|
|
168
|
-
flush()
|
|
169
|
-
if current and current_units >= min_chars and current_units + units > max_chars:
|
|
170
|
-
flush()
|
|
171
|
-
current.append(piece)
|
|
172
|
-
current_scene_ids.update(piece_scene_ids)
|
|
173
|
-
current_units += units
|
|
174
|
-
if current_units >= max_chars:
|
|
175
|
-
flush()
|
|
176
|
-
flush()
|
|
177
|
-
return chunks
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
19
|
+
def _find_scene_for_midpoint(scenes_analysis, start, end):
|
|
20
|
+
mid = (start + end) / 2
|
|
21
|
+
for scene in scenes_analysis:
|
|
22
|
+
if scene["start"] <= mid <= scene["end"]:
|
|
23
|
+
return scene
|
|
24
|
+
return None
|
|
238
25
|
|
|
239
26
|
|
|
240
27
|
def _scene_available_seconds(start, end):
|
|
@@ -251,51 +38,15 @@ def _recommended_char_budget(start, end):
|
|
|
251
38
|
return int(_scene_available_seconds(start, end) * effective_rate)
|
|
252
39
|
|
|
253
40
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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)
|
|
41
|
+
# Lint-only realism on top of the brief's budget. Every block is one TTS utterance, and the
|
|
42
|
+
# provider pads each utterance with edge silence that voiceover keeps and assemble must
|
|
43
|
+
# place inside the window. Measured on 98 MiMo blocks from real runs (-40 dBFS): 0.40 /
|
|
44
|
+
# 0.54 / 0.62 s at p10 / p50 / p90 before the 1.15x narration atempo, i.e. about 0.47 s
|
|
45
|
+
# placed. On a 2-3 s window that is a fifth of the room, so without it a short window
|
|
46
|
+
# passed lint and failed in assemble as no_safe_fit after TTS was billed.
|
|
47
|
+
TTS_UTTERANCE_OVERHEAD_SECONDS = 0.45
|
|
298
48
|
|
|
299
49
|
|
|
300
|
-
def
|
|
301
|
-
|
|
50
|
+
def _lint_char_budget(start, end):
|
|
51
|
+
"""The brief's budget for the window minus one utterance's TTS edge silence."""
|
|
52
|
+
return _recommended_char_budget(start, end - TTS_UTTERANCE_OVERHEAD_SECONDS)
|
|
@@ -61,7 +61,11 @@ def _text_units(text: str) -> int:
|
|
|
61
61
|
def _normalise_segments(payload: list[dict[str, Any]], *, source: str) -> list[dict[str, Any]]:
|
|
62
62
|
key = "narration" if source == "narration" else "text"
|
|
63
63
|
segments: list[dict[str, Any]] = []
|
|
64
|
+
# `index` is the item's position in the file as written (non-objects included), so a
|
|
65
|
+
# blocker points at the same entry the author sees there.
|
|
64
66
|
for idx, item in enumerate(payload):
|
|
67
|
+
if not isinstance(item, dict):
|
|
68
|
+
continue
|
|
65
69
|
text = str(item.get(key, "")).strip()
|
|
66
70
|
if text:
|
|
67
71
|
segments.append({"source": source, "index": idx, "text": text})
|
|
@@ -77,46 +81,27 @@ def _load_original_subtitles(work_dir: Path | None) -> list[dict[str, Any]]:
|
|
|
77
81
|
return _normalise_segments(json.loads(path.read_text(encoding="utf-8")), source="original_subtitles")
|
|
78
82
|
|
|
79
83
|
|
|
80
|
-
def
|
|
81
|
-
"""
|
|
82
|
-
if work_dir is None:
|
|
83
|
-
return False, "legacy_default"
|
|
84
|
-
path = work_dir / "deslop_qc_requirements.json"
|
|
85
|
-
if not path.exists():
|
|
86
|
-
return False, "legacy_default"
|
|
87
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
88
|
-
return data["style_card_required"], "deslop_qc_requirements.json"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _style_card_issue(work_dir: Path | None, required: bool) -> dict[str, Any] | None:
|
|
84
|
+
def _style_card_issue(work_dir: Path | None) -> dict[str, Any] | None:
|
|
85
|
+
"""A missing or empty style_card.json is always advisory, never a render blocker."""
|
|
92
86
|
if work_dir is None:
|
|
93
87
|
return None
|
|
94
|
-
severity = "blocker" if required else "advisory"
|
|
95
88
|
path = work_dir / "style_card.json"
|
|
96
89
|
if not path.exists():
|
|
97
90
|
return {
|
|
98
|
-
"severity":
|
|
91
|
+
"severity": "advisory",
|
|
99
92
|
"code": "missing_style_card",
|
|
100
93
|
"source": "style_card",
|
|
101
94
|
"index": None,
|
|
102
|
-
"message":
|
|
103
|
-
"style_card.json is required by this expression/packaging run but is missing"
|
|
104
|
-
if required else
|
|
105
|
-
"style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it"
|
|
106
|
-
),
|
|
95
|
+
"message": "style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it",
|
|
107
96
|
}
|
|
108
97
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
109
98
|
if not isinstance(data, dict) or not data:
|
|
110
99
|
return {
|
|
111
|
-
"severity":
|
|
100
|
+
"severity": "advisory",
|
|
112
101
|
"code": "malformed_style_card",
|
|
113
102
|
"source": "style_card",
|
|
114
103
|
"index": None,
|
|
115
|
-
"message":
|
|
116
|
-
"style_card.json is required but empty or not a JSON object"
|
|
117
|
-
if required else
|
|
118
|
-
"style_card.json is present but empty or not a JSON object; treat as migration warning unless the run requires it"
|
|
119
|
-
),
|
|
104
|
+
"message": "style_card.json is present but empty or not a JSON object; treat as migration warning",
|
|
120
105
|
}
|
|
121
106
|
return None
|
|
122
107
|
|
|
@@ -139,10 +124,9 @@ def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path |
|
|
|
139
124
|
blockers: list[dict[str, Any]] = []
|
|
140
125
|
advisories: list[dict[str, Any]] = []
|
|
141
126
|
|
|
142
|
-
|
|
143
|
-
style_issue = _style_card_issue(work_path, required)
|
|
127
|
+
style_issue = _style_card_issue(work_path)
|
|
144
128
|
if style_issue:
|
|
145
|
-
|
|
129
|
+
advisories.append(style_issue)
|
|
146
130
|
|
|
147
131
|
all_text = "\n".join(seg["text"] for seg in segments)
|
|
148
132
|
total_units = max(1, _text_units(all_text))
|
|
@@ -189,8 +173,6 @@ def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path |
|
|
|
189
173
|
"ok": not blockers,
|
|
190
174
|
"contract": CONTRACT,
|
|
191
175
|
"scanner": "deslop_qc.py",
|
|
192
|
-
"style_card_required": required,
|
|
193
|
-
"style_card_requirement_source": requirement_source,
|
|
194
176
|
"blocker_count": len(blockers),
|
|
195
177
|
"advisory_count": len(advisories),
|
|
196
178
|
"blockers": blockers,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Where an interrupting narration block may be told to move.
|
|
2
|
+
|
|
3
|
+
`interrupts_source_sentence` suggests a sentence-end anchor the whole block can shift to.
|
|
4
|
+
These helpers keep that suggestion between the block's neighbours and clear of every other
|
|
5
|
+
block, and decide when a block is a back-to-back handoff that needs no entry check.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_number(value):
|
|
12
|
+
return (
|
|
13
|
+
isinstance(value, (int, float))
|
|
14
|
+
and not isinstance(value, bool)
|
|
15
|
+
and math.isfinite(value)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# A narration block that starts within this gap after another block's end is a
|
|
20
|
+
# back-to-back handoff: the source track is never exposed between the two.
|
|
21
|
+
CONNECTED_HANDOFF_SECONDS = 0.15
|
|
22
|
+
# A suggested entry farther than this from the authored one moves the block off the
|
|
23
|
+
# pictures it was written for; past it the author must rethink the block instead.
|
|
24
|
+
SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS = 10.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _shift_is_clear(new_start, duration, other_spans):
|
|
28
|
+
"""The block moved whole to new_start neither overlaps nor abuts another block."""
|
|
29
|
+
new_end = new_start + duration
|
|
30
|
+
gap = CONNECTED_HANDOFF_SECONDS
|
|
31
|
+
return all(
|
|
32
|
+
new_end < other_start - gap or new_start > other_end + gap
|
|
33
|
+
for other_start, other_end in other_spans
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _nearest_safe_anchor(start, end, anchors, other_spans, window=(None, None)):
|
|
38
|
+
"""The anchor closest to `start` the whole block can move to (duration kept) without
|
|
39
|
+
overlapping or abutting another block or leaving `window`, within
|
|
40
|
+
SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS; a later anchor wins a tie. None when none qualifies.
|
|
41
|
+
|
|
42
|
+
`window` is the open (low, high) range the moved block must sit strictly inside: the
|
|
43
|
+
neighbours' ends with the handoff gap, so a block never jumps over a neighbour (story
|
|
44
|
+
order) and two adjacent blocks never get colliding suggestions. Either side may be None.
|
|
45
|
+
"""
|
|
46
|
+
duration = end - start
|
|
47
|
+
low, high = window
|
|
48
|
+
candidates = [
|
|
49
|
+
anchor
|
|
50
|
+
for anchor in anchors
|
|
51
|
+
if abs(anchor["time"] - start) <= SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS
|
|
52
|
+
and anchor["time"] >= 0
|
|
53
|
+
and (low is None or anchor["time"] > low)
|
|
54
|
+
and (high is None or anchor["time"] + duration < high)
|
|
55
|
+
and _shift_is_clear(anchor["time"], duration, other_spans)
|
|
56
|
+
]
|
|
57
|
+
if not candidates:
|
|
58
|
+
return None
|
|
59
|
+
return min(candidates, key=lambda anchor: (abs(anchor["time"] - start), -anchor["time"]))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _valid_span(seg):
|
|
63
|
+
"""(start, end) of a block with a usable numeric window, else None."""
|
|
64
|
+
if not isinstance(seg, dict):
|
|
65
|
+
return None
|
|
66
|
+
start, end = seg.get("start"), seg.get("end")
|
|
67
|
+
if _is_number(start) and _is_number(end) and end > start:
|
|
68
|
+
return start, end
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _other_block_spans(narration, idx):
|
|
73
|
+
"""(start, end) of every other block with a usable numeric window."""
|
|
74
|
+
return [
|
|
75
|
+
span
|
|
76
|
+
for other_idx, other in enumerate(narration)
|
|
77
|
+
if other_idx != idx and (span := _valid_span(other))
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Clip/timeline edges are allowed positions; the open window bound sits this far outside.
|
|
82
|
+
_EDGE_EPSILON = 1e-6
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _suggestion_window(narration, idx, suggested_spans, limits=(None, None)):
|
|
86
|
+
"""Open (low, high) range a suggested move of block `idx` must stay strictly inside.
|
|
87
|
+
|
|
88
|
+
Bounded by the previous and next valid blocks (narration is chronological) with the
|
|
89
|
+
handoff gap. The previous block counts at both its authored and its own suggested
|
|
90
|
+
window, so applying any subset of the suggestions keeps adjacent blocks apart and in
|
|
91
|
+
order. `limits` is the closed (first, last) range the block must stay in: its clip in
|
|
92
|
+
cut mode, the output duration in cut_output; either side may be None.
|
|
93
|
+
"""
|
|
94
|
+
gap = CONNECTED_HANDOFF_SECONDS
|
|
95
|
+
low = high = None
|
|
96
|
+
for prev_idx in range(idx - 1, -1, -1):
|
|
97
|
+
span = _valid_span(narration[prev_idx])
|
|
98
|
+
if span:
|
|
99
|
+
low = max(span[1], suggested_spans.get(prev_idx, span)[1]) + gap
|
|
100
|
+
break
|
|
101
|
+
for next_idx in range(idx + 1, len(narration)):
|
|
102
|
+
span = _valid_span(narration[next_idx])
|
|
103
|
+
if span:
|
|
104
|
+
high = span[0] - gap
|
|
105
|
+
break
|
|
106
|
+
first, last = limits
|
|
107
|
+
if first is not None:
|
|
108
|
+
low = max(x for x in (low, first - _EDGE_EPSILON) if x is not None)
|
|
109
|
+
if last is not None:
|
|
110
|
+
high = min(x for x in (high, last + _EDGE_EPSILON) if x is not None)
|
|
111
|
+
return low, high
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _has_connected_predecessor(narration, idx, start):
|
|
115
|
+
"""A back-to-back narration handoff (<=150ms gap) does not expose the source track,
|
|
116
|
+
so the next TTS block is not a new source-speech entry."""
|
|
117
|
+
for other_idx, other in enumerate(narration):
|
|
118
|
+
if other_idx == idx or not isinstance(other, dict):
|
|
119
|
+
continue
|
|
120
|
+
other_start, other_end = other.get("start"), other.get("end")
|
|
121
|
+
if not _is_number(other_start) or not _is_number(other_end):
|
|
122
|
+
continue
|
|
123
|
+
if other_start < start and -0.001 <= start - other_end <= CONNECTED_HANDOFF_SECONDS:
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
@@ -7,13 +7,11 @@ import re
|
|
|
7
7
|
|
|
8
8
|
EVIDENCE_CONTRACT_VERSION = 1
|
|
9
9
|
|
|
10
|
-
COVERAGE_POLICY_VERSION = "coverage_policy_v1"
|
|
11
|
-
|
|
12
10
|
|
|
13
11
|
def _safe_time(item, key, default=0.0):
|
|
14
12
|
try:
|
|
15
13
|
value = float(item.get(key, default))
|
|
16
|
-
except (
|
|
14
|
+
except (TypeError, ValueError):
|
|
17
15
|
return default
|
|
18
16
|
return value if math.isfinite(value) else default
|
|
19
17
|
|
|
@@ -85,7 +83,6 @@ def coverage_policy_v1(
|
|
|
85
83
|
duration = _overall_duration(scenes, asr_result, narration)
|
|
86
84
|
if duration <= 0:
|
|
87
85
|
return {
|
|
88
|
-
"coverage_policy_version": COVERAGE_POLICY_VERSION,
|
|
89
86
|
"selected_ranges": [],
|
|
90
87
|
"dropped_ranges": [],
|
|
91
88
|
"dropped_range_count": 0,
|
|
@@ -162,7 +159,6 @@ def coverage_policy_v1(
|
|
|
162
159
|
for r in merged + dropped:
|
|
163
160
|
r.pop("priority", None)
|
|
164
161
|
return {
|
|
165
|
-
"coverage_policy_version": COVERAGE_POLICY_VERSION,
|
|
166
162
|
"selected_ranges": merged,
|
|
167
163
|
"dropped_ranges": dropped,
|
|
168
164
|
"dropped_range_count": len(dropped),
|
|
@@ -276,13 +272,11 @@ def _research_context_items(research):
|
|
|
276
272
|
|
|
277
273
|
|
|
278
274
|
def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="source"):
|
|
279
|
-
"""Pure
|
|
275
|
+
"""Pure: collect visual/ASR evidence items inside ranges."""
|
|
280
276
|
clock = "output" if timeline == "cut_output" else "source"
|
|
281
277
|
items = []
|
|
282
278
|
dropped = {"visual": 0, "asr": 0}
|
|
283
279
|
for i, scene in enumerate(vlm_analysis or []):
|
|
284
|
-
if not isinstance(scene, dict):
|
|
285
|
-
continue
|
|
286
280
|
start = _safe_time(scene, "start")
|
|
287
281
|
end = _safe_time(scene, "end", start)
|
|
288
282
|
if end <= start or not _in_ranges(start, end, ranges):
|
|
@@ -309,8 +303,6 @@ def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="sou
|
|
|
309
303
|
item[k] = scene.get(k)
|
|
310
304
|
items.append(item)
|
|
311
305
|
for i, seg in enumerate(asr_result or []):
|
|
312
|
-
if not isinstance(seg, dict):
|
|
313
|
-
continue
|
|
314
306
|
text = str(seg.get("text", "")).strip()
|
|
315
307
|
if not text:
|
|
316
308
|
continue
|
|
@@ -346,22 +338,6 @@ def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="sou
|
|
|
346
338
|
}
|
|
347
339
|
|
|
348
340
|
|
|
349
|
-
def build_review_coverage_metadata(bundle):
|
|
350
|
-
"""Summarize a build_evidence_bundle() bundle's coverage contract."""
|
|
351
|
-
coverage = bundle["coverage"]
|
|
352
|
-
metadata = bundle["metadata"]
|
|
353
|
-
return {
|
|
354
|
-
"coverage_policy_version": coverage["coverage_policy_version"],
|
|
355
|
-
"time_ranges": coverage["selected_ranges"],
|
|
356
|
-
"dropped_ranges": coverage["dropped_ranges"],
|
|
357
|
-
"dropped_range_count": coverage["dropped_range_count"],
|
|
358
|
-
"scene_count": metadata["reviewed_scene_count"],
|
|
359
|
-
"asr_count": metadata["reviewed_asr_count"],
|
|
360
|
-
"dropped_scene_count": metadata["dropped_scene_count"],
|
|
361
|
-
"dropped_asr_count": metadata["dropped_asr_count"],
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
341
|
def build_evidence_bundle(
|
|
366
342
|
vlm_analysis,
|
|
367
343
|
asr_result,
|