@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
|
@@ -50,44 +50,30 @@ def _load_review_grounding(work_dir):
|
|
|
50
50
|
"""
|
|
51
51
|
work_dir = Path(work_dir)
|
|
52
52
|
manifest = _load(work_dir, "multi_source_manifest.json")
|
|
53
|
-
|
|
54
|
-
if isinstance(sources, list):
|
|
53
|
+
if manifest is not None:
|
|
55
54
|
combined_vlm = []
|
|
56
55
|
combined_asr = []
|
|
57
|
-
for source in sources:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
source_id = str(source.get("source_id") or "").strip()
|
|
61
|
-
relative_dir = source.get("source_work_dir")
|
|
62
|
-
if not source_id or not relative_dir:
|
|
63
|
-
continue
|
|
64
|
-
source_dir = (work_dir / str(relative_dir)).resolve(strict=False)
|
|
56
|
+
for source in manifest["sources"]:
|
|
57
|
+
source_id = str(source["source_id"])
|
|
58
|
+
source_dir = (work_dir / source["source_work_dir"]).resolve(strict=False)
|
|
65
59
|
try:
|
|
66
60
|
source_dir.relative_to(work_dir.resolve(strict=False))
|
|
67
61
|
except ValueError:
|
|
68
62
|
continue
|
|
69
63
|
for scene in _load(source_dir, "vlm_analysis.json") or []:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
combined_vlm.append(item)
|
|
74
|
-
clean_asr = _load(source_dir, "asr_clean.json")
|
|
75
|
-
raw_asr = (
|
|
76
|
-
clean_asr
|
|
77
|
-
if clean_asr is not None
|
|
78
|
-
else _load(source_dir, "asr_result.json")
|
|
79
|
-
)
|
|
80
|
-
for segment in _asr_segments(raw_asr):
|
|
81
|
-
if isinstance(segment, dict):
|
|
82
|
-
item = dict(segment)
|
|
83
|
-
item["source_id"] = source_id
|
|
84
|
-
combined_asr.append(item)
|
|
64
|
+
combined_vlm.append({**scene, "source_id": source_id})
|
|
65
|
+
for segment in _preferred_asr(source_dir):
|
|
66
|
+
combined_asr.append({**segment, "source_id": source_id})
|
|
85
67
|
if combined_vlm or combined_asr:
|
|
86
68
|
return combined_vlm, combined_asr
|
|
87
69
|
|
|
88
|
-
return _load(work_dir, "vlm_analysis.json") or [],
|
|
89
|
-
|
|
90
|
-
|
|
70
|
+
return _load(work_dir, "vlm_analysis.json") or [], _preferred_asr(work_dir)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _preferred_asr(work_dir):
|
|
74
|
+
"""asr_clean.json wins over asr_result.json, as in lint, cut and assemble."""
|
|
75
|
+
clean = _load(work_dir, "asr_clean.json")
|
|
76
|
+
return _asr_segments(clean if clean is not None else _load(work_dir, "asr_result.json"))
|
|
91
77
|
|
|
92
78
|
|
|
93
79
|
def _load_cut_clip_spans(work_dir):
|
|
@@ -176,13 +162,8 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
|
|
|
176
162
|
|
|
177
163
|
remapped_scenes = []
|
|
178
164
|
for scene in vlm_analysis or []:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
try:
|
|
182
|
-
start = float(scene.get("start", 0))
|
|
183
|
-
end = float(scene.get("end", start))
|
|
184
|
-
except (TypeError, ValueError):
|
|
185
|
-
continue
|
|
165
|
+
start = float(scene["start"])
|
|
166
|
+
end = float(scene["end"])
|
|
186
167
|
overlaps = _source_output_overlaps(
|
|
187
168
|
start,
|
|
188
169
|
end,
|
|
@@ -205,13 +186,8 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
|
|
|
205
186
|
|
|
206
187
|
remapped_asr = []
|
|
207
188
|
for seg in asr_result or []:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
start = float(seg.get("start", 0))
|
|
212
|
-
end = float(seg.get("end", start))
|
|
213
|
-
except (TypeError, ValueError):
|
|
214
|
-
continue
|
|
189
|
+
start = float(seg["start"])
|
|
190
|
+
end = float(seg["end"])
|
|
215
191
|
text = str(seg.get("text", "")).strip()
|
|
216
192
|
if not text:
|
|
217
193
|
continue
|
|
@@ -231,8 +207,6 @@ def remap_grounding_to_output_timeline(vlm_analysis, asr_result, clip_spans):
|
|
|
231
207
|
item["output_segment_index"] = overlap.get("output_segment_index")
|
|
232
208
|
remapped_asr.append(item)
|
|
233
209
|
|
|
234
|
-
remapped_scenes.sort(
|
|
235
|
-
|
|
236
|
-
)
|
|
237
|
-
remapped_asr.sort(key=lambda x: (float(x.get("start", 0)), float(x.get("end", 0))))
|
|
210
|
+
remapped_scenes.sort(key=lambda x: (x["start"], x["end"]))
|
|
211
|
+
remapped_asr.sort(key=lambda x: (x["start"], x["end"]))
|
|
238
212
|
return remapped_scenes, remapped_asr
|
|
@@ -13,7 +13,6 @@ from evidence_bundle import (
|
|
|
13
13
|
_in_ranges,
|
|
14
14
|
_safe_time,
|
|
15
15
|
build_evidence_bundle,
|
|
16
|
-
build_review_coverage_metadata,
|
|
17
16
|
render_evidence_bundle,
|
|
18
17
|
)
|
|
19
18
|
from review_grounding import _load_agent_optional
|
|
@@ -42,48 +41,26 @@ CATEGORIES = [
|
|
|
42
41
|
"other",
|
|
43
42
|
]
|
|
44
43
|
|
|
45
|
-
SCORECARD_KEYS = [
|
|
46
|
-
"promise_match",
|
|
47
|
-
"hook_3s",
|
|
48
|
-
"first_15s_delivery",
|
|
49
|
-
"spine_clarity",
|
|
50
|
-
"stakes_escalation",
|
|
51
|
-
"information_gain",
|
|
52
|
-
"spoken_language",
|
|
53
|
-
"sentence_brevity",
|
|
54
|
-
"tts_pacing",
|
|
55
|
-
"grounding",
|
|
56
|
-
"original_audio_use",
|
|
57
|
-
"subtitle_readability",
|
|
58
|
-
"ending_payoff",
|
|
59
|
-
"style_consistency",
|
|
60
|
-
"ai_flavor",
|
|
61
|
-
"packaging_consistency",
|
|
62
|
-
]
|
|
63
|
-
|
|
64
44
|
FACTUAL_CATEGORIES = {"hallucination", "incomplete"}
|
|
65
45
|
|
|
66
|
-
COVERAGE_POLICY_VERSION = "coverage_policy_v1"
|
|
67
|
-
|
|
68
46
|
RUBRIC = """你是中文视频解说的创作复核编辑。依据素材证据和已有创作计划审阅草稿,只指出真实问题,宁缺毋滥:
|
|
69
47
|
1. 反幻觉(最重要):解说里的人物、动作、因果、关系必须由带标签的 evidence 支撑。画面/对白是 timeline evidence(clock=SOURCE 或 OUTPUT);背景资料/user_context 只能作为 context-only(clock=null)辅助识别/消歧。research-only 不能升级成当前画面强事实;若与 research 一致但画面/对白里看不到,最多 severity=suggestion/category=grounding_risk,不要判 error;只有与全部可得证据矛盾才是 severity=error, category=hallucination,并指出冲突证据。
|
|
70
48
|
2. 导演意图:若提供 recap_story_plan.json,检查草稿是否兑现 viewer promise、POV、dramatic question、情绪路径和 chosen_hypothesis;不要另起一条更“吸睛”但不属于该计划的故事。偏离主线 → no_throughline;承诺不兑现 → promise_mismatch/weak_payoff。
|
|
71
49
|
3. change-based beats:每个 beat 应改变知识、权力、目标、关系、情绪或风险。精简时不能只留下“发生了什么”,还要保住人物动机、接受条件及随后犹豫/行动的必要前提。若一段只重复上一段、删除后什么都不损失,可报 low_information_gain/pacing;不要用固定段数或秒数代替判断。
|
|
72
50
|
4. 钩子:开头要提出正文真实兑现的戏剧问题/利害,不是交代场景,也不是无关的留存话术。弱钩子 → weak_hook。
|
|
73
51
|
5. 给信息而非念画面:观众看得见动作表情;解说只增加上下文、因果、预期、证据支持的解释或跨越。复述画面 → narrating_picture。
|
|
74
|
-
6. 视听分工:若提供 visual_audio_board.json,检查 narration_job=none 或 audio_owner=original_dialogue/action_sound/ambience/music/silence 的拍是否被旁白无故覆盖;必须听见的原声被盖住 → original_audio_conflict
|
|
52
|
+
6. 视听分工:若提供 visual_audio_board.json,检查 narration_job=none 或 audio_owner=original_dialogue/action_sound/ambience/music/silence 的拍是否被旁白无故覆盖;必须听见的原声被盖住 → original_audio_conflict。沉默和低旁白覆盖本身不是问题。这些拍与 original_subtitles 的原声字幕块按计划没有旁白,不是跳过或漏写。
|
|
75
53
|
7. 人物、反应与动作兑现:不要用旁白解释掉素材中已经能成立的表演、停顿或反应。有情感回应或知情变化的反应镜不可机械删;反打是否保留取决于它是否提供新增信息,而非是否达到统一时长。以某个可见结果为看点时,只写 evidence 已呈现的结果,不推断更强的结果(例:证据只到受击,就不能写成倒地或胜负)。当前评审只能检查计划/稿件一致性,不能凭少量帧声称最终剪点一定好坏。
|
|
76
54
|
8. 密度/节奏与因果边界:7:3 不是配额。只在旁白没有任务、墙到墙压住原声、碎成一句一停,或无意长空档导致因果断裂时,报 density/pacing。不得把跨场镜头拼成同场动作/反应的虚假因果,也不用花字替补源证据或提前宣布结果。
|
|
77
55
|
9. 去废词:删空泛形容("危机四伏""震撼人心")→ cliche。
|
|
78
|
-
10.
|
|
56
|
+
10. 完整句子:旁白本身半句话/未收尾 → incomplete。某拍没有旁白不算 incomplete。
|
|
79
57
|
11. 段落衔接:解说块要为随后的原声留白铺垫,下一块要承接原声刚呈现的变化;若两块各说各的、原声进来接不上 → disjoint_handoff。
|
|
80
58
|
12. 结尾回收:结尾要兑现开头承诺/主线情绪,不要突然停、只复述最后画面、没有情绪/信息回报;弱回收 → weak_payoff。
|
|
81
59
|
13. 风格一致性与修改范围:若提供 style_card.json,把它当作表达意图/语气/节奏边界;不符合意图 → style_mismatch。不要把 style_card 当标题/封面/首句包装计划。只提能定位到具体段落、beat 或镜头边界的具体局部修法;REVISION 未点名层默认冻结,不借局部问题重做故事、声音或包装。
|
|
82
60
|
14. 包装一致性:只有提供 packaging_plan.json 时才评估标题/封面/首句/卖点承诺与正文兑现;缺失不扣分。不一致 → packaging_mismatch。不要让包装反过来改写故事判断。
|
|
83
|
-
15. 去AI味:若出现模板化、空泛拔高、过度对仗、机械转折、明显 agent 示例残留,可报 ai_flavor;若出现示例人物/占位实体泄漏(如未替换示例名、模板角色)→ example_entity_leak。“不是 A,而是 B”本身不是错误,只有在先虚构旧判断再制造假洞察、或反复机械使用时才建议改写。
|
|
84
|
-
另外给一份内容效果 scorecard(1-5,advisory:除事实矛盾/残句外不要据此给 error;缺项可以省略,系统会保留为 null/未评分):promise_match/hook_3s/first_15s_delivery/spine_clarity/stakes_escalation/information_gain/spoken_language/sentence_brevity/tts_pacing/grounding/original_audio_use/subtitle_readability/ending_payoff/style_consistency/ai_flavor/packaging_consistency。`sentence_brevity` 衡量的是句子是否简洁而完整,不是越短越高;连续短句造成串珠式 TTS 应降低分数。ai_flavor 分数含义:5=自然、人味强,1=AI味明显。
|
|
61
|
+
15. 去AI味:若出现模板化、空泛拔高、过度对仗、机械转折、明显 agent 示例残留,可报 ai_flavor;若出现示例人物/占位实体泄漏(如未替换示例名、模板角色)→ example_entity_leak。“不是 A,而是 B”本身不是错误,只有在先虚构旧判断再制造假洞察、或反复机械使用时才建议改写。
|
|
85
62
|
只返回 JSON(不要额外解释),格式:
|
|
86
|
-
{"verdict":"PASS|REVISE|FAIL","summary":"一两句总体判断","
|
|
63
|
+
{"verdict":"PASS|REVISE|FAIL","summary":"一两句总体判断","findings":[{"segment":<草稿段号(从0起)或null表示整体>,"severity":"error|warning|suggestion","category":"<上面类别之一>","issue":"问题","fix":"具体改法"}]}"""
|
|
87
64
|
|
|
88
65
|
|
|
89
66
|
def merge_review_findings(chunks):
|
|
@@ -173,7 +150,7 @@ def _merge_chunk_reviews(chunk_reviews):
|
|
|
173
150
|
return parse_review_response("")
|
|
174
151
|
if len(chunk_reviews) == 1:
|
|
175
152
|
return chunk_reviews[0]
|
|
176
|
-
verdict_rank = {"PASS": 0, "
|
|
153
|
+
verdict_rank = {"PASS": 0, "REVISE": 1, "FAIL": 2}
|
|
177
154
|
best = max(chunk_reviews, key=lambda r: verdict_rank[r["verdict"]])
|
|
178
155
|
merged = dict(best)
|
|
179
156
|
merged["findings"] = merge_review_findings(chunk_reviews)
|
|
@@ -189,33 +166,6 @@ def _merge_chunk_reviews(chunk_reviews):
|
|
|
189
166
|
return merged
|
|
190
167
|
|
|
191
168
|
|
|
192
|
-
def _research_guardrail_qc(review, context_items):
|
|
193
|
-
# Assertion keys are whatever the judge returned; only the list itself is normalized.
|
|
194
|
-
assertions = review["grounding_assertions"]
|
|
195
|
-
research_context_assertions = [
|
|
196
|
-
a
|
|
197
|
-
for a in assertions
|
|
198
|
-
if str(a.get("source", "")).lower() == "research"
|
|
199
|
-
or a.get("support") == "context_only"
|
|
200
|
-
or a.get("clock") is None
|
|
201
|
-
]
|
|
202
|
-
risk_assertions = [
|
|
203
|
-
a
|
|
204
|
-
for a in research_context_assertions
|
|
205
|
-
if "spoiler" in str(a.get("risk", "")).lower()
|
|
206
|
-
or "research-only" in str(a.get("risk", "")).lower()
|
|
207
|
-
or "current-timeline" in str(a.get("risk", "")).lower()
|
|
208
|
-
]
|
|
209
|
-
grounding_risk_findings = [
|
|
210
|
-
f for f in review["findings"] if f["category"] == "grounding_risk"
|
|
211
|
-
]
|
|
212
|
-
return {
|
|
213
|
-
"context_only_assertions": len(context_items) + len(research_context_assertions),
|
|
214
|
-
"spoiler_risk_assertions": len(risk_assertions) + len(grounding_risk_findings),
|
|
215
|
-
"policy": "research evidence is context_only unless visual/asr-supported",
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
169
|
def _load_review_research_context(work_dir):
|
|
220
170
|
"""Agent-authored background_research.json: {} when absent; corrupt JSON raises
|
|
221
171
|
(same policy as the brief's loader), a non-object document fails open to {}."""
|
|
@@ -317,41 +267,89 @@ def _format_json_context(title, value, limit=3000):
|
|
|
317
267
|
return f"## {title}\n{text[:limit]}"
|
|
318
268
|
|
|
319
269
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
270
|
+
# audio_owner values that leave a beat to the source track (playbook: visual_audio_board).
|
|
271
|
+
ORIGINAL_AUDIO_OWNERS = frozenset(
|
|
272
|
+
("original_dialogue", "action_sound", "ambience", "music", "silence")
|
|
273
|
+
)
|
|
274
|
+
_MAX_ORIGINAL_AUDIO_LINES = 40
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _beat_window(beat, clock):
|
|
278
|
+
"""(label, start, end) on `clock` when the beat carries it, else on the other clock."""
|
|
279
|
+
for name in (clock, "source" if clock == "output" else "output"):
|
|
280
|
+
start, end = beat.get(f"{name}_start"), beat.get(f"{name}_end")
|
|
281
|
+
if (
|
|
282
|
+
isinstance(start, (int, float))
|
|
283
|
+
and isinstance(end, (int, float))
|
|
284
|
+
and not isinstance(start, bool)
|
|
285
|
+
and not isinstance(end, bool)
|
|
286
|
+
and end > start
|
|
287
|
+
):
|
|
288
|
+
return name.upper(), float(start), float(end)
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _original_audio_beats(board, plan):
|
|
293
|
+
"""Beats the plan leaves to the source track: audio_owner is original audio or
|
|
294
|
+
narration_job is none. visual_audio_board owns audio decisions, so a story-plan beat
|
|
295
|
+
counts only when the board does not list the same beat_id."""
|
|
296
|
+
board_items = board.get("items") if isinstance(board, dict) else None
|
|
297
|
+
plan_beats = plan.get("beats") if isinstance(plan, dict) else None
|
|
298
|
+
board_items = [b for b in board_items or [] if isinstance(b, dict)]
|
|
299
|
+
board_ids = {str(b.get("beat_id")) for b in board_items if b.get("beat_id") is not None}
|
|
300
|
+
candidates = board_items + [
|
|
301
|
+
b
|
|
302
|
+
for b in plan_beats or []
|
|
303
|
+
if isinstance(b, dict) and str(b.get("beat_id")) not in board_ids
|
|
304
|
+
]
|
|
305
|
+
return [
|
|
306
|
+
beat
|
|
307
|
+
for beat in candidates
|
|
308
|
+
if str(beat.get("audio_owner", "")).strip() in ORIGINAL_AUDIO_OWNERS
|
|
309
|
+
or str(beat.get("narration_job", "")).strip() == "none"
|
|
310
|
+
]
|
|
337
311
|
|
|
338
|
-
def _normalise_list_of_dicts(value, allowed_keys):
|
|
339
|
-
out = []
|
|
340
|
-
for item in value or []:
|
|
341
|
-
if isinstance(item, dict):
|
|
342
|
-
out.append({key: item.get(key) for key in allowed_keys if key in item})
|
|
343
|
-
return out
|
|
344
312
|
|
|
313
|
+
def _format_original_audio_holds(board, plan, subtitles, clock):
|
|
314
|
+
"""Name every interval the plan deliberately leaves without narration.
|
|
345
315
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if
|
|
353
|
-
|
|
354
|
-
|
|
316
|
+
The planning JSON is clipped to a few thousand characters, so without this list a
|
|
317
|
+
reviewer can see a story beat with no narration and report it as skipped.
|
|
318
|
+
"""
|
|
319
|
+
lines = []
|
|
320
|
+
for beat in _original_audio_beats(board, plan):
|
|
321
|
+
window = _beat_window(beat, clock)
|
|
322
|
+
when = f"{window[0]} {window[1]:.1f}-{window[2]:.1f}s" if window else "时间未标"
|
|
323
|
+
owner = str(beat.get("audio_owner", "")).strip() or "?"
|
|
324
|
+
job = str(beat.get("narration_job", "")).strip() or "?"
|
|
325
|
+
anchor = _clip_text(
|
|
326
|
+
beat.get("original_audio_anchor") or beat.get("must_keep_moment"), 60
|
|
327
|
+
)
|
|
328
|
+
tail = f" 原声锚点:{anchor}" if anchor else ""
|
|
329
|
+
lines.append(
|
|
330
|
+
f"- [{when}] beat {beat.get('beat_id', '?')} audio_owner={owner} narration_job={job}{tail}"
|
|
331
|
+
)
|
|
332
|
+
for row in subtitles if isinstance(subtitles, list) else []:
|
|
333
|
+
if not isinstance(row, dict):
|
|
334
|
+
continue
|
|
335
|
+
start, end = _safe_time(row, "start", None), _safe_time(row, "end", None)
|
|
336
|
+
text = _clip_text(row.get("text"), 60)
|
|
337
|
+
if start is None or end is None or end <= start or not text:
|
|
338
|
+
continue
|
|
339
|
+
lines.append(f"- [{clock.upper()} {start:.1f}-{end:.1f}s] 原声字幕块「{text}」")
|
|
340
|
+
if not lines:
|
|
341
|
+
return ""
|
|
342
|
+
dropped = len(lines) - _MAX_ORIGINAL_AUDIO_LINES
|
|
343
|
+
lines = lines[:_MAX_ORIGINAL_AUDIO_LINES]
|
|
344
|
+
if dropped > 0:
|
|
345
|
+
lines.append(f"- …另有 {dropped} 条同类区间未列出")
|
|
346
|
+
return (
|
|
347
|
+
"## 计划内留给原声的区间(不是漏写)\n"
|
|
348
|
+
"以下拍在 visual_audio_board/recap_story_plan 里由原声或沉默拥有,或是 original_subtitles 的原声字幕块。"
|
|
349
|
+
"这些区间没有旁白是有意的:不要报为跳过、缺失或漏写,也不要据此给 incomplete、no_throughline、"
|
|
350
|
+
"low_information_gain 或 density。只有旁白闯入并盖住必须听见的原声时报 original_audio_conflict;"
|
|
351
|
+
"相邻旁白块与这段原声接不上时报 disjoint_handoff。\n" + "\n".join(lines)
|
|
352
|
+
)
|
|
355
353
|
|
|
356
354
|
|
|
357
355
|
def _format_draft(narration):
|
|
@@ -393,27 +391,33 @@ def build_review_messages(
|
|
|
393
391
|
research=research_obj,
|
|
394
392
|
)
|
|
395
393
|
evidence_text = render_evidence_bundle(bundle)
|
|
396
|
-
# Optional, agent-authored planning
|
|
397
|
-
# existing fail-open optional artifact behavior; these only sharpen
|
|
394
|
+
# Optional, agent-authored planning artifacts. Bad JSON returns None via _load, matching
|
|
395
|
+
# existing fail-open optional artifact behavior; these only sharpen the craft findings.
|
|
398
396
|
packaging = _load_optional_json(work_dir, "packaging_plan.json")
|
|
399
397
|
story_plan = _load_optional_json(work_dir, "recap_story_plan.json")
|
|
400
398
|
av_board = _load_optional_json(work_dir, "visual_audio_board.json")
|
|
401
399
|
style_card = _load_optional_json(work_dir, "style_card.json")
|
|
402
|
-
|
|
400
|
+
holds = _format_original_audio_holds(
|
|
401
|
+
av_board,
|
|
402
|
+
story_plan,
|
|
403
|
+
_load_optional_json(work_dir, "original_subtitles.json"),
|
|
404
|
+
bundle["clock"],
|
|
405
|
+
)
|
|
406
|
+
holds_block = f"{holds}\n\n" if holds else ""
|
|
403
407
|
user = (
|
|
404
408
|
f"{RUBRIC}\n\n"
|
|
405
|
-
"##
|
|
406
|
-
"若 work_dir 提供了 packaging_plan/recap_story_plan/visual_audio_board/style_card
|
|
409
|
+
"## 创作计划参考\n"
|
|
410
|
+
"若 work_dir 提供了 packaging_plan/recap_story_plan/visual_audio_board/style_card,则结合评审:"
|
|
407
411
|
"packaging_plan 只负责标题/封面/首句/卖点承诺与正文兑现;style_card 只负责表达意图、语气、节奏和禁忌;"
|
|
408
|
-
"recap_story_plan 是导演意图/备选假设/chosen POV/change-based beats 的基线;visual_audio_board 是画面/表演/原声/audio_owner/narration_job
|
|
409
|
-
"
|
|
412
|
+
"recap_story_plan 是导演意图/备选假设/chosen POV/change-based beats 的基线;visual_audio_board 是画面/表演/原声/audio_owner/narration_job/剪辑锚点的基线。"
|
|
413
|
+
"若未提供,则基于解说本身与画面/对白证据评审,但不得因计划文件缺失给 error。统一评估:hook 是否真实兑现;每段是否产生变化/信息增量而非看图说话;"
|
|
410
414
|
"结尾是否兑现开头承诺/主线情绪;是否写给耳朵听(连续完整思路、自然口语、TTS可呼吸,而非一句一停);人物/关系/因果断言是否有 visual/ASR timeline evidence;research/user_context 仅可作为 context-only 辅助。\n"
|
|
411
415
|
"审美/风格/包装/去AI味项是 advisory:可 REVISE,但除事实矛盾/残句外不要给 error。\n\n"
|
|
412
416
|
f"{_format_json_context('packaging_plan.json(标题/封面/首句/卖点包装承诺,可能为空)', packaging)}\n\n"
|
|
413
417
|
f"{_format_json_context('recap_story_plan.json(主线/beats/original moments,可能为空)', story_plan)}\n\n"
|
|
414
418
|
f"{_format_json_context('visual_audio_board.json(画面/原声/字幕/剪辑锚点,可能为空)', av_board)}\n\n"
|
|
415
419
|
f"{_format_json_context('style_card.json(表达意图/语气/节奏/禁忌,不负责包装,可能为空)', style_card)}\n\n"
|
|
416
|
-
f"{
|
|
420
|
+
f"{holds_block}"
|
|
417
421
|
f"## 背景资料(context-only/advisory:只辅助识别/消歧/弱背景,不是当前画面强事实)\n"
|
|
418
422
|
f"Guardrail: clock=null/context_only;不得把未来剧情或 research-only 关系/因果升级为当前事实。\n"
|
|
419
423
|
f"{research_context or '(无)'}\n\n"
|
|
@@ -423,23 +427,6 @@ def build_review_messages(
|
|
|
423
427
|
return [{"role": "user", "content": user}]
|
|
424
428
|
|
|
425
429
|
|
|
426
|
-
def _downgrade_context_assertions(assertions):
|
|
427
|
-
out = []
|
|
428
|
-
for item in assertions or []:
|
|
429
|
-
src = str(item.get("source", "")).strip().lower()
|
|
430
|
-
if src in {"research", "user_context"}:
|
|
431
|
-
item = dict(item)
|
|
432
|
-
item["support"] = "context_only"
|
|
433
|
-
item["clock"] = None
|
|
434
|
-
risk = str(item.get("risk", "")).strip()
|
|
435
|
-
label = "research-only" if src == "research" else "user_context-only"
|
|
436
|
-
item["risk"] = (
|
|
437
|
-
risk + "; " if risk else ""
|
|
438
|
-
) + f"{label}: advisory/context_only, not a strong current-timeline fact"
|
|
439
|
-
out.append(item)
|
|
440
|
-
return out
|
|
441
|
-
|
|
442
|
-
|
|
443
430
|
def parse_review_response(text):
|
|
444
431
|
"""Pure: robustly extract the reviewer JSON; fall back to a REVISE-unknown shell."""
|
|
445
432
|
raw = str(text or "")
|
|
@@ -466,8 +453,10 @@ def parse_review_response(text):
|
|
|
466
453
|
|
|
467
454
|
def _normalise_review(data):
|
|
468
455
|
verdict = str(data.get("verdict", "REVISE")).upper()
|
|
469
|
-
# PASS/REVISE/FAIL
|
|
470
|
-
if verdict
|
|
456
|
+
# The prompt asks for PASS/REVISE/FAIL only; a stray "OK" from the judge means approval.
|
|
457
|
+
if verdict == "OK":
|
|
458
|
+
verdict = "PASS"
|
|
459
|
+
if verdict not in ("PASS", "REVISE", "FAIL"):
|
|
471
460
|
verdict = "REVISE"
|
|
472
461
|
findings = []
|
|
473
462
|
for f in data.get("findings", []) or []:
|
|
@@ -493,42 +482,29 @@ def _normalise_review(data):
|
|
|
493
482
|
"fix": str(f.get("fix", "")).strip(),
|
|
494
483
|
}
|
|
495
484
|
)
|
|
496
|
-
# The scorecard and the lists below are PURE ADVISORY enrichment: they never mutate the
|
|
497
|
-
# judge's verdict (deliberately not the colleague's auto-downgrade) and never gate. The
|
|
498
|
-
# hard pre-TTS gate stays exactly where it was — error findings counted in recap.py.
|
|
499
485
|
return {
|
|
500
486
|
"verdict": verdict,
|
|
501
487
|
"summary": str(data.get("summary", "")).strip(),
|
|
502
|
-
"scorecard": _normalise_scorecard(data.get("scorecard")),
|
|
503
|
-
"hook_candidates_review": _normalise_list_of_dicts(
|
|
504
|
-
data.get("hook_candidates_review"),
|
|
505
|
-
["candidate", "type", "score", "keep", "reason"],
|
|
506
|
-
),
|
|
507
|
-
"retention_risk_points": _normalise_list_of_dicts(
|
|
508
|
-
data.get("retention_risk_points"), ["time", "risk", "fix", "evidence"]
|
|
509
|
-
),
|
|
510
|
-
"highest_return_edits": _normalise_string_list(
|
|
511
|
-
data.get("highest_return_edits")
|
|
512
|
-
),
|
|
513
|
-
"information_gain_notes": _normalise_list_of_dicts(
|
|
514
|
-
data.get("information_gain_notes"), ["segment", "label", "note", "rewrite"]
|
|
515
|
-
),
|
|
516
|
-
"spoken_language_rewrites": _normalise_list_of_dicts(
|
|
517
|
-
data.get("spoken_language_rewrites"),
|
|
518
|
-
["segment", "original", "rewrite", "why"],
|
|
519
|
-
),
|
|
520
|
-
"grounding_assertions": _downgrade_context_assertions(
|
|
521
|
-
_normalise_list_of_dicts(
|
|
522
|
-
data.get("grounding_assertions"),
|
|
523
|
-
["segment", "assertion", "source", "risk", "support", "clock"],
|
|
524
|
-
)
|
|
525
|
-
),
|
|
526
488
|
"findings": findings,
|
|
527
489
|
}
|
|
528
490
|
|
|
529
491
|
|
|
492
|
+
def _segment_label(segment):
|
|
493
|
+
"""The draft lists blocks from 0 (`segment` keeps that index); readers see 段 N from 1."""
|
|
494
|
+
if segment is None:
|
|
495
|
+
return "整体"
|
|
496
|
+
if isinstance(segment, str) and segment.strip().isdigit():
|
|
497
|
+
segment = int(segment)
|
|
498
|
+
elif isinstance(segment, float) and segment.is_integer():
|
|
499
|
+
segment = int(segment)
|
|
500
|
+
if isinstance(segment, int) and not isinstance(segment, bool) and segment >= 0:
|
|
501
|
+
return f"段 {segment + 1}"
|
|
502
|
+
# Any other shape is not a block index; show the raw value, never as a 段 number.
|
|
503
|
+
return f"段 ?(模型返回 {json.dumps(segment, ensure_ascii=False, default=str)})"
|
|
504
|
+
|
|
505
|
+
|
|
530
506
|
def format_review_md(review):
|
|
531
|
-
"""Render a parse_review_response() review
|
|
507
|
+
"""Render a parse_review_response() review as markdown."""
|
|
532
508
|
order = {"error": 0, "warning": 1, "suggestion": 2}
|
|
533
509
|
findings = sorted(review["findings"], key=lambda f: order[f["severity"]])
|
|
534
510
|
counts = {
|
|
@@ -543,127 +519,13 @@ def format_review_md(review):
|
|
|
543
519
|
"",
|
|
544
520
|
review["summary"] or "_(no summary)_",
|
|
545
521
|
"",
|
|
546
|
-
"##
|
|
522
|
+
"## Findings",
|
|
547
523
|
]
|
|
548
|
-
for key, v in review["scorecard"].items():
|
|
549
|
-
out.append(f"- {key}: {v}/5" if v is not None else f"- {key}: 未评分")
|
|
550
|
-
out.extend(["", "## Highest-return edits"])
|
|
551
|
-
out.extend([f"- {edit}" for edit in review["highest_return_edits"]] or ["- (none)"])
|
|
552
|
-
out.extend(["", "## Retention risk points"])
|
|
553
|
-
risks = review["retention_risk_points"]
|
|
554
|
-
if risks:
|
|
555
|
-
for item in risks:
|
|
556
|
-
out.append(
|
|
557
|
-
f"- {item.get('time', '?')}: {item.get('risk', '')} — {item.get('fix', '')}"
|
|
558
|
-
)
|
|
559
|
-
else:
|
|
560
|
-
out.append("- (none)")
|
|
561
|
-
out.extend(["", "## Hook candidates review"])
|
|
562
|
-
hooks = review["hook_candidates_review"]
|
|
563
|
-
if hooks:
|
|
564
|
-
for item in hooks:
|
|
565
|
-
out.append(
|
|
566
|
-
f"- {item.get('type', '?')} {item.get('score', '-')}/5: {item.get('candidate', '')} ({'keep' if item.get('keep') else 'drop'}) {item.get('reason', '')}"
|
|
567
|
-
)
|
|
568
|
-
else:
|
|
569
|
-
out.append("- (none)")
|
|
570
|
-
out.extend(
|
|
571
|
-
["", "## Information gain / write-for-ear / grounding", "### Information gain"]
|
|
572
|
-
)
|
|
573
|
-
notes = review["information_gain_notes"]
|
|
574
|
-
out.extend(
|
|
575
|
-
[
|
|
576
|
-
f"- 段 {n.get('segment')}: {n.get('label')} — {n.get('note', '')} {n.get('rewrite', '')}"
|
|
577
|
-
for n in notes
|
|
578
|
-
]
|
|
579
|
-
or ["- (none)"]
|
|
580
|
-
)
|
|
581
|
-
out.append("### Spoken rewrites")
|
|
582
|
-
rewrites = review["spoken_language_rewrites"]
|
|
583
|
-
out.extend(
|
|
584
|
-
[
|
|
585
|
-
f"- 段 {r.get('segment')}: {r.get('original', '')} → {r.get('rewrite', '')}({r.get('why', '')})"
|
|
586
|
-
for r in rewrites
|
|
587
|
-
]
|
|
588
|
-
or ["- (none)"]
|
|
589
|
-
)
|
|
590
|
-
out.append("### Grounding assertions")
|
|
591
|
-
assertions = review["grounding_assertions"]
|
|
592
|
-
out.extend(
|
|
593
|
-
[
|
|
594
|
-
f"- 段 {a.get('segment')}: {a.get('assertion', '')} [{a.get('source', '')}] {a.get('risk', '')}"
|
|
595
|
-
for a in assertions
|
|
596
|
-
]
|
|
597
|
-
or ["- (none)"]
|
|
598
|
-
)
|
|
599
|
-
out.extend(["", "## Findings"])
|
|
600
524
|
if not findings:
|
|
601
525
|
out.append("- (none)")
|
|
602
526
|
for f in findings:
|
|
603
|
-
seg =
|
|
527
|
+
seg = _segment_label(f["segment"])
|
|
604
528
|
out.append(f"- **[{f['severity']}/{f['category']}] {seg}** — {f['issue']}")
|
|
605
529
|
if f["fix"]:
|
|
606
530
|
out.append(f" - 改法: {f['fix']}")
|
|
607
531
|
return "\n".join(out) + "\n"
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
def build_grounding_qc(work_dir, review, bundle, *, timeline="source"):
|
|
611
|
-
"""Pure-ish compatibility seam: build grounding QC payload without writing it.
|
|
612
|
-
|
|
613
|
-
It reads optional QC sidecars from work_dir to preserve the existing artifact
|
|
614
|
-
contract, but has no side effects.
|
|
615
|
-
"""
|
|
616
|
-
work_dir = Path(work_dir)
|
|
617
|
-
items = bundle["items"]
|
|
618
|
-
context = bundle["context_items"]
|
|
619
|
-
# The runner hands the same pipeline warnings to the bundle and the review.
|
|
620
|
-
warnings = list(bundle["warnings"])
|
|
621
|
-
verdict = "warn" if warnings else "pass"
|
|
622
|
-
if any(f["severity"] == "error" for f in review["findings"]):
|
|
623
|
-
verdict = "fail"
|
|
624
|
-
coverage_meta = build_review_coverage_metadata(bundle)
|
|
625
|
-
visual_items = [item for item in items if item["source"] == "visual"]
|
|
626
|
-
asr_items = [item for item in items if item["source"] == "asr"]
|
|
627
|
-
return {
|
|
628
|
-
"schema_version": 1,
|
|
629
|
-
"owner": "video-script.review",
|
|
630
|
-
"timeline": timeline,
|
|
631
|
-
"coverage_policy_version": COVERAGE_POLICY_VERSION,
|
|
632
|
-
"review_coverage": {
|
|
633
|
-
"time_ranges": coverage_meta["time_ranges"],
|
|
634
|
-
"scene_count": coverage_meta["scene_count"],
|
|
635
|
-
"asr_count": coverage_meta["asr_count"],
|
|
636
|
-
"dropped_ranges": coverage_meta["dropped_ranges"],
|
|
637
|
-
},
|
|
638
|
-
"evidence_contract": {
|
|
639
|
-
# Every timeline item carries the bundle clock; context items are unclocked.
|
|
640
|
-
"source_items": len(items) if bundle["clock"] == "source" else 0,
|
|
641
|
-
"output_items": len(items) if bundle["clock"] == "output" else 0,
|
|
642
|
-
"unclocked_items": 0,
|
|
643
|
-
"context_only_items": len(context),
|
|
644
|
-
},
|
|
645
|
-
"index_inputs": {
|
|
646
|
-
"vlm": bool(visual_items),
|
|
647
|
-
"asr": bool(asr_items),
|
|
648
|
-
"research": (work_dir / "background_research.json").exists(),
|
|
649
|
-
},
|
|
650
|
-
"speech_window_qc": _load_optional_json(work_dir, "silence_periods.qc.json")
|
|
651
|
-
or {"coarse_asr_windows": 0, "low_confidence_speech_flags": 0},
|
|
652
|
-
"research_guardrail": _research_guardrail_qc(review, context),
|
|
653
|
-
"warnings": warnings,
|
|
654
|
-
"verdict": verdict,
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
def write_grounding_qc(work_dir, qc):
|
|
659
|
-
"""Compatibility seam: write a prebuilt grounding_qc.json payload."""
|
|
660
|
-
work_dir = Path(work_dir)
|
|
661
|
-
(work_dir / "grounding_qc.json").write_text(
|
|
662
|
-
json.dumps(qc, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
663
|
-
)
|
|
664
|
-
return qc
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
def _write_grounding_qc(work_dir, review, bundle, *, timeline="source"):
|
|
668
|
-
qc = build_grounding_qc(work_dir, review, bundle, timeline=timeline)
|
|
669
|
-
return write_grounding_qc(work_dir, qc)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Run narration review and write
|
|
1
|
+
"""Run narration review and write narration_review.json / .md."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
from lib import CONFIG, log, api_call
|
|
11
11
|
|
|
12
|
-
from evidence_bundle import build_evidence_bundle
|
|
12
|
+
from evidence_bundle import EVIDENCE_CONTRACT_VERSION, build_evidence_bundle
|
|
13
13
|
from review_grounding import (
|
|
14
14
|
_load_cut_clip_spans,
|
|
15
15
|
_load_required,
|
|
@@ -20,16 +20,11 @@ from review_response import (
|
|
|
20
20
|
_chunk_evidence_bundle,
|
|
21
21
|
_load_review_research_context,
|
|
22
22
|
_merge_chunk_reviews,
|
|
23
|
-
_write_grounding_qc,
|
|
24
23
|
build_review_messages,
|
|
25
24
|
format_review_md,
|
|
26
25
|
parse_review_response,
|
|
27
26
|
)
|
|
28
27
|
|
|
29
|
-
EVIDENCE_CONTRACT_VERSION = 1
|
|
30
|
-
|
|
31
|
-
COVERAGE_POLICY_VERSION = "coverage_policy_v1"
|
|
32
|
-
|
|
33
28
|
|
|
34
29
|
def review_narration(work_dir, *, timeline="source", strict_evidence=False):
|
|
35
30
|
work_dir = Path(work_dir)
|
|
@@ -94,13 +89,10 @@ def review_narration(work_dir, *, timeline="source", strict_evidence=False):
|
|
|
94
89
|
"schema_version": EVIDENCE_CONTRACT_VERSION,
|
|
95
90
|
"timeline": timeline,
|
|
96
91
|
"clock": bundle["clock"],
|
|
97
|
-
"coverage_policy_version": COVERAGE_POLICY_VERSION,
|
|
98
92
|
"selected_ranges": bundle["coverage"]["selected_ranges"],
|
|
99
93
|
"chunk_count": len(chunks),
|
|
100
94
|
"warnings": warnings,
|
|
101
95
|
}
|
|
102
|
-
_write_grounding_qc(work_dir, review, bundle, timeline=timeline)
|
|
103
|
-
|
|
104
96
|
(work_dir / "narration_review.json").write_text(
|
|
105
97
|
json.dumps(review, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
106
98
|
)
|