@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
|
@@ -154,6 +154,44 @@ def _remap_frame_facts_for_brief(frame_facts, overlap):
|
|
|
154
154
|
return out
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
# A time the VLM quoted in its prose: "12.0s" (the frame-tag form), "12秒", "01:05",
|
|
158
|
+
# "1:02:03". Only a value inside the scene's own source range counts as a source timestamp;
|
|
159
|
+
# anything else (a duration, an on-screen clock) is left as written.
|
|
160
|
+
_PROSE_TIME_RE = re.compile(
|
|
161
|
+
r"(?<![\d.:])(?:(?:(\d+):)?(\d{1,2}):(\d{2}(?:\.\d+)?)(?![\d:])"
|
|
162
|
+
r"|(\d+(?:\.\d+)?)\s*(?:s|秒)(?![A-Za-z]))"
|
|
163
|
+
)
|
|
164
|
+
_PROSE_TIME_SLACK = 0.25
|
|
165
|
+
_CUT_AWAY_TIME = "[cut-away moment]"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _remap_prose_times_for_brief(text, scene, overlap):
|
|
169
|
+
"""Rewrite source timestamps quoted in VLM prose onto the OUTPUT clock.
|
|
170
|
+
|
|
171
|
+
A time inside this part's kept source range becomes its output time ("3.2s"); a time
|
|
172
|
+
elsewhere in the scene was cut away and is replaced by a marker, so an OUTPUT section
|
|
173
|
+
never quotes a source-clock time."""
|
|
174
|
+
|
|
175
|
+
def replace(match):
|
|
176
|
+
hours, minutes, secs, bare = match.groups()
|
|
177
|
+
if bare is not None:
|
|
178
|
+
when = float(bare)
|
|
179
|
+
else:
|
|
180
|
+
when = int(hours or 0) * 3600 + int(minutes) * 60 + float(secs)
|
|
181
|
+
if not (scene["start"] - _PROSE_TIME_SLACK <= when <= scene["end"] + _PROSE_TIME_SLACK):
|
|
182
|
+
return match.group(0)
|
|
183
|
+
if not (
|
|
184
|
+
overlap["source_start"] - _PROSE_TIME_SLACK
|
|
185
|
+
<= when
|
|
186
|
+
<= overlap["source_end"] + _PROSE_TIME_SLACK
|
|
187
|
+
):
|
|
188
|
+
return _CUT_AWAY_TIME
|
|
189
|
+
out = overlap["output_start"] + (when - overlap["source_start"])
|
|
190
|
+
return f"{min(max(out, overlap['output_start']), overlap['output_end']):.1f}s"
|
|
191
|
+
|
|
192
|
+
return _PROSE_TIME_RE.sub(replace, text) if isinstance(text, str) else text
|
|
193
|
+
|
|
194
|
+
|
|
157
195
|
def _remap_scenes_to_output_for_brief(scenes, spans):
|
|
158
196
|
out = []
|
|
159
197
|
for scene in scenes:
|
|
@@ -165,6 +203,9 @@ def _remap_scenes_to_output_for_brief(scenes, spans):
|
|
|
165
203
|
item["frame_facts"] = _remap_frame_facts_for_brief(
|
|
166
204
|
scene.get("frame_facts", {}), overlap
|
|
167
205
|
)
|
|
206
|
+
for field in ("description", "depth_analysis"):
|
|
207
|
+
if field in item:
|
|
208
|
+
item[field] = _remap_prose_times_for_brief(item[field], scene, overlap)
|
|
168
209
|
if len(overlaps) > 1:
|
|
169
210
|
item["scene_id"] = f"{scene['scene_id']}.{part_idx}"
|
|
170
211
|
out.append(item)
|
|
@@ -186,6 +227,38 @@ def _remap_segments_to_output_for_brief(segments, spans):
|
|
|
186
227
|
return out
|
|
187
228
|
|
|
188
229
|
|
|
230
|
+
_COVERAGE_SLACK = 0.05
|
|
231
|
+
# No "." inside: the ASR chunker splits sentences on it.
|
|
232
|
+
_PARTIAL_WINDOW_TEXT = "[partial ASR window: only part of it is in the cut, text withheld]"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _remap_asr_to_output_for_brief(segments, spans):
|
|
236
|
+
"""ASR rows on the OUTPUT clock for the brief's writing evidence.
|
|
237
|
+
|
|
238
|
+
A coarse window the cut keeps only part of cannot say which of its words survived, so
|
|
239
|
+
each kept piece shows a partial-window marker instead of the window's text: the
|
|
240
|
+
brief never quotes dialogue that may not be in the cut. A window the kept clips cover
|
|
241
|
+
completely (even across a seam) keeps its text. `speech_spans` in
|
|
242
|
+
speech_boundary_anchors_output.json are remapped separately and keep the raw text."""
|
|
243
|
+
out = []
|
|
244
|
+
for seg in segments:
|
|
245
|
+
overlaps = _source_output_overlaps_for_brief(seg["start"], seg["end"], spans)
|
|
246
|
+
kept = sum(o["source_end"] - o["source_start"] for o in overlaps)
|
|
247
|
+
window = seg["end"] - seg["start"]
|
|
248
|
+
partial = bool(str(seg.get("text") or "").strip()) and kept < window - _COVERAGE_SLACK
|
|
249
|
+
for overlap in overlaps:
|
|
250
|
+
item = dict(seg)
|
|
251
|
+
item["start"] = round(overlap["output_start"], 3)
|
|
252
|
+
item["end"] = round(overlap["output_end"], 3)
|
|
253
|
+
if "duration" in item:
|
|
254
|
+
item["duration"] = round(item["end"] - item["start"], 3)
|
|
255
|
+
if partial:
|
|
256
|
+
item["text"] = _PARTIAL_WINDOW_TEXT
|
|
257
|
+
out.append(item)
|
|
258
|
+
out.sort(key=lambda x: (x["start"], x["end"]))
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
189
262
|
def _remap_brief_evidence_to_output_timeline(
|
|
190
263
|
work_dir, scenes_analysis, asr_result, silence_periods, *, required=False
|
|
191
264
|
):
|
|
@@ -194,7 +267,7 @@ def _remap_brief_evidence_to_output_timeline(
|
|
|
194
267
|
return scenes_analysis, asr_result, silence_periods
|
|
195
268
|
return (
|
|
196
269
|
_remap_scenes_to_output_for_brief(scenes_analysis, spans),
|
|
197
|
-
|
|
270
|
+
_remap_asr_to_output_for_brief(asr_result, spans),
|
|
198
271
|
_remap_segments_to_output_for_brief(silence_periods, spans),
|
|
199
272
|
)
|
|
200
273
|
|
|
@@ -204,6 +277,13 @@ def _read_json(path, default):
|
|
|
204
277
|
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else default
|
|
205
278
|
|
|
206
279
|
|
|
280
|
+
def _source_to_span_output(source_time, span):
|
|
281
|
+
"""`source_time` on one clip span's OUTPUT clock, clamped to that span: the next span on
|
|
282
|
+
the output timeline plays a different source range."""
|
|
283
|
+
when = min(max(source_time, span["source_start"]), span["source_end"])
|
|
284
|
+
return round(min(span["output_start"] + when - span["source_start"], span["output_end"]), 3)
|
|
285
|
+
|
|
286
|
+
|
|
207
287
|
def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
|
|
208
288
|
"""Load sentence anchors and remap them to cut OUTPUT time when needed."""
|
|
209
289
|
work_dir = Path(work_dir)
|
|
@@ -217,26 +297,41 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
|
|
|
217
297
|
return anchors
|
|
218
298
|
|
|
219
299
|
spans = _load_cut_output_spans_for_brief(work_dir, required=True)
|
|
300
|
+
output_duration = max(span["output_end"] for span in spans)
|
|
220
301
|
remapped = []
|
|
221
302
|
for anchor in anchors:
|
|
222
303
|
source_time = anchor["time"]
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
304
|
+
# A pause end a few ms past a frame-snapped clip edge belongs to that clip, at the
|
|
305
|
+
# edge; only when no clip plays the instant itself.
|
|
306
|
+
owners = [span for span in spans
|
|
307
|
+
if span["source_start"] <= source_time <= span["source_end"]] or [
|
|
308
|
+
span for span in spans
|
|
309
|
+
if span["source_start"] - 0.05 <= source_time <= span["source_end"] + 0.05]
|
|
310
|
+
for span in owners:
|
|
311
|
+
# The 0.05 s match slack can carry an anchor past the output's ends (a clip
|
|
312
|
+
# end frame-snapped 181.42 -> 181.40): narration cannot start there.
|
|
313
|
+
unclamped = round(span["output_start"] + source_time - span["source_start"], 3)
|
|
314
|
+
if not 0 <= unclamped <= output_duration:
|
|
315
|
+
continue
|
|
316
|
+
item = dict(anchor)
|
|
317
|
+
item["source_time"] = round(source_time, 3)
|
|
318
|
+
item["time"] = _source_to_span_output(source_time, span)
|
|
319
|
+
# Preserve the measured safe pause in OUTPUT time too, so cut-mode lint
|
|
320
|
+
# compares one clock.
|
|
321
|
+
source_pause_start = max(
|
|
322
|
+
span["source_start"], min(anchor.get("pause_start", source_time - 0.12), source_time)
|
|
323
|
+
)
|
|
324
|
+
item["source_pause_start"] = round(min(source_pause_start, span["source_end"]), 3)
|
|
325
|
+
item["pause_start"] = _source_to_span_output(source_pause_start, span)
|
|
326
|
+
# `time` IS the pause end; keep the source value apart so the artifact
|
|
327
|
+
# never mixes clocks.
|
|
328
|
+
item["source_pause_end"] = round(anchor.get("pause_end", source_time), 3)
|
|
329
|
+
item["pause_end"] = item["time"]
|
|
330
|
+
if "expected_time" in anchor:
|
|
331
|
+
expected = float(anchor["expected_time"])
|
|
332
|
+
item["source_expected_time"] = round(expected, 3)
|
|
333
|
+
item["expected_time"] = _source_to_span_output(expected, span)
|
|
334
|
+
remapped.append(item)
|
|
240
335
|
|
|
241
336
|
speech_rows = [
|
|
242
337
|
row for row in _read_json(work_dir / "asr_result.json", []) if row["text"]
|
|
@@ -262,19 +357,30 @@ def _sentence_entry_anchors_for_brief(work_dir, edit_mode):
|
|
|
262
357
|
return out_payload["sentence_anchors"]
|
|
263
358
|
|
|
264
359
|
|
|
360
|
+
def _anchor_boundary_use(anchor):
|
|
361
|
+
# Schema-1 anchors (no `boundary_use`) came from the old coarse estimator: high/medium
|
|
362
|
+
# labels there are usable but unverified.
|
|
363
|
+
return anchor.get("boundary_use") or (
|
|
364
|
+
"unverified" if anchor["confidence"] in {"high", "medium"} else "none"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
265
368
|
def _format_sentence_entry_anchors_for_brief(work_dir, edit_mode):
|
|
266
369
|
anchors = [
|
|
267
370
|
anchor
|
|
268
371
|
for anchor in _sentence_entry_anchors_for_brief(work_dir, edit_mode)
|
|
269
|
-
if anchor
|
|
372
|
+
if _anchor_boundary_use(anchor) != "none"
|
|
270
373
|
]
|
|
271
374
|
if not anchors:
|
|
272
375
|
return []
|
|
273
376
|
lines = [
|
|
274
377
|
"## 原声句末安全切入点",
|
|
275
378
|
"",
|
|
276
|
-
"这些时间是 ASR
|
|
379
|
+
"这些时间是 ASR 句末标点与短声学停顿对齐后的旁白入口。旁白在原声已开始后切入时,"
|
|
277
380
|
"必须从其中一个点开始;否则会在 TTS 前被 `interrupts_source_sentence` 硬阻断。",
|
|
381
|
+
"- 标 `unverified` 的点是由粗粒度 ASR 窗口里的标点位置估计、再吸附到短停顿得到的:"
|
|
382
|
+
"落在停顿里,不会切断单词,但不保证原声句子已经说完(误差上限见 `±`)。请自己听一下再定;"
|
|
383
|
+
"门禁本身照常生效。",
|
|
278
384
|
"- 调整方式:优先把 `start` 移到建议锚点;放不下时缩短文本、移动整块或删除该旁白,不能让脚本静默挪音频。",
|
|
279
385
|
'- 原声句子完整性是硬约束:切入块写 `"source_entry_policy": "sentence_boundary"`;'
|
|
280
386
|
"不存在 `intentional_interrupt` 绕过方式。没有后续可靠句末锚点时,移动、缩短或删除旁白块。",
|
|
@@ -284,8 +390,17 @@ def _format_sentence_entry_anchors_for_brief(work_dir, edit_mode):
|
|
|
284
390
|
f" (SOURCE {anchor['source_time']:.2f}s)" if "source_time" in anchor else ""
|
|
285
391
|
)
|
|
286
392
|
text_tail = str(anchor.get("text_tail", "")).strip()
|
|
393
|
+
if _anchor_boundary_use(anchor) == "verified":
|
|
394
|
+
label = anchor["confidence"]
|
|
395
|
+
else:
|
|
396
|
+
# Schema-1 anchors carry no error bound to print.
|
|
397
|
+
label = (
|
|
398
|
+
f"unverified ±{float(anchor['timing_bound_seconds']):.1f}s"
|
|
399
|
+
if "timing_bound_seconds" in anchor
|
|
400
|
+
else "unverified"
|
|
401
|
+
)
|
|
287
402
|
lines.append(
|
|
288
|
-
f"- {anchor['time']:.2f}s [{
|
|
403
|
+
f"- {anchor['time']:.2f}s [{label}]{source_suffix} {text_tail}".rstrip()
|
|
289
404
|
)
|
|
290
405
|
lines.append("")
|
|
291
406
|
return lines
|
|
@@ -308,19 +423,3 @@ def _format_output_clip_list(work_dir):
|
|
|
308
423
|
)
|
|
309
424
|
out.append("")
|
|
310
425
|
return out if len(out) > 2 else []
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def _write_deslop_qc_requirements(work_dir):
|
|
314
|
-
"""Write the stable deslop QC contract consumed by deslop_qc.py.
|
|
315
|
-
|
|
316
|
-
style_card_required defaults to False (advisory): a missing style_card.json is a
|
|
317
|
-
warning, not a render-blocking error. A future opt-in run can set it True to make
|
|
318
|
-
style_card.json a hard requirement — deslop_qc.py reads that field.
|
|
319
|
-
"""
|
|
320
|
-
payload = {
|
|
321
|
-
"schema_version": 1,
|
|
322
|
-
"style_card_required": False,
|
|
323
|
-
}
|
|
324
|
-
path = Path(work_dir) / "deslop_qc_requirements.json"
|
|
325
|
-
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
326
|
-
return path
|
|
@@ -10,7 +10,8 @@ Optional, synchronous, in-pipeline. Two independent LLM passes over the video's
|
|
|
10
10
|
cleanup pass can never shift the timing-bearing spans downstream chunking depends on.
|
|
11
11
|
|
|
12
12
|
Both passes are idempotent (a fresh artifact is reused); a failure raises and the runner
|
|
13
|
-
records it in consolidation.status.json.
|
|
13
|
+
records it in consolidation.status.json. A cut-off or non-JSON model reply is a failure too
|
|
14
|
+
(ConsolidateIncomplete), never an empty artifact reported as done. Mirrors review.py's pure-seam + thin-driver shape so it is
|
|
14
15
|
unit-testable with a mocked api_call. NON-required: the pipeline runs unchanged without it.
|
|
15
16
|
"""
|
|
16
17
|
|
|
@@ -20,6 +21,7 @@ import re
|
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
|
|
22
23
|
from lib import CONFIG, log, api_call, file_identity, load_background_research
|
|
24
|
+
from index_normalize import coerce_character_aliases, normalize_index, scenes_end
|
|
23
25
|
from understanding_cache import _fresh
|
|
24
26
|
|
|
25
27
|
# Shared tolerance for the per-segment span check. The brief-side gate inlines the SAME
|
|
@@ -28,6 +30,29 @@ from understanding_cache import _fresh
|
|
|
28
30
|
_ASR_SPAN_TOL = 0.05
|
|
29
31
|
ASR_CLEAN_POSTPROCESS_VERSION = 2
|
|
30
32
|
|
|
33
|
+
# Output budgets. A 5-min recap's index ran past 3000 tokens and came back cut off
|
|
34
|
+
# (finish_reason=length); a cut-off reply parses to nothing, so it must never be written as
|
|
35
|
+
# a finished artifact. One retry doubles the budget; a second cut-off raises.
|
|
36
|
+
_CLEAN_MAX_TOKENS = 8000
|
|
37
|
+
_INDEX_MAX_TOKENS = 8000
|
|
38
|
+
_RETRY_BUDGET_FACTOR = 2
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConsolidateIncomplete(RuntimeError):
|
|
42
|
+
"""The model reply was cut off or is not JSON; nothing was written for that pass."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConsolidateNoKey(RuntimeError):
|
|
46
|
+
"""A pass needs the model but no API key is set; nothing was sent or written."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require_api_key(label):
|
|
50
|
+
"""Offline runs reuse fresh artifacts; a pass that would call the model skips instead of
|
|
51
|
+
sending an unauthenticated request that can only 401."""
|
|
52
|
+
if not CONFIG["api_key"]:
|
|
53
|
+
log(f"consolidate({label}): 未设置 {CONFIG['api_env_var']},跳过(不发送请求)")
|
|
54
|
+
raise ConsolidateNoKey(label)
|
|
55
|
+
|
|
31
56
|
CLEAN_PROMPT = """你在清洗中文视频的 ASR 逐段转写。对【每一段】做:补标点、修明显同音/错别字、(能判断时)在句首轻标说话人,让长段连读文本变成清晰可读的句子。
|
|
32
57
|
铁律:
|
|
33
58
|
- 不要合并或拆分段落,输出段数必须与输入完全一致,顺序一致。
|
|
@@ -44,6 +69,7 @@ INDEX_PROMPT = """你在根据逐场景画面分析、ASR对白、background_res
|
|
|
44
69
|
- visual/asr 是当前视频事实证据;每个人物、关系、剧情节点、物件尽量给 evidence_ids。
|
|
45
70
|
- background_research 只能用于人名/别名/术语/身份消歧,默认 support=context_only;不能把后续剧情或未出现关系升级为当前画面事实。
|
|
46
71
|
- ASR 中出现但画面描述未命名的人名,应进入 characters[*].asr_mentions。
|
|
72
|
+
- 输出要紧凑、必须是完整 JSON:只收对解说有用的角色/关系/节点;description/relation/text 每条不超过 40 字;visual_descriptions 最多 3 条;每项 evidence_ids 最多 6 个;plot_points 最多 20 条。
|
|
47
73
|
只返回 JSON:
|
|
48
74
|
{"characters":[{"name":"角色名或外观指代","description":"身份/特征","aliases":[],"visual_descriptions":[],"asr_mentions":[],"research_role":"","evidence_ids":[],"confidence":"high|medium|low"}],
|
|
49
75
|
"relationships":[{"a":"角色","b":"角色","relation":"关系","evidence_ids":[],"support":"direct|indirect|context_only"}],
|
|
@@ -339,6 +365,7 @@ def parse_index_response(text):
|
|
|
339
365
|
):
|
|
340
366
|
val = data.get(key)
|
|
341
367
|
out[key] = val if isinstance(val, list) else []
|
|
368
|
+
out["characters"] = coerce_character_aliases(out["characters"])
|
|
342
369
|
for item in out["research_glossary"]:
|
|
343
370
|
if isinstance(item, dict):
|
|
344
371
|
item["support"] = "context_only"
|
|
@@ -479,15 +506,10 @@ def consolidate_transcript(work_dir):
|
|
|
479
506
|
):
|
|
480
507
|
log("consolidate(asr): asr_clean.json 已最新,跳过")
|
|
481
508
|
return existing
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
"messages": build_clean_messages(asr_result),
|
|
486
|
-
"max_tokens": 4000,
|
|
487
|
-
"temperature": 0.2,
|
|
488
|
-
}
|
|
509
|
+
_require_api_key("asr")
|
|
510
|
+
content = _complete_json_reply(
|
|
511
|
+
"asr", build_clean_messages(asr_result), _CLEAN_MAX_TOKENS
|
|
489
512
|
)
|
|
490
|
-
content = _response_text(resp)
|
|
491
513
|
segments = parse_clean_response(content, asr_result)
|
|
492
514
|
payload = {
|
|
493
515
|
"source": _input_identity(work_dir, "asr_result.json"),
|
|
@@ -513,62 +535,124 @@ def consolidate_index(work_dir):
|
|
|
513
535
|
if _fresh(out_path, work_dir / "vlm_analysis.json") and _index_cache_matches(
|
|
514
536
|
work_dir, vlm_analysis
|
|
515
537
|
):
|
|
538
|
+
cached = _load(work_dir, "understanding_index.json")
|
|
539
|
+
repaired = _normalized_index(cached, vlm_analysis) if isinstance(cached, dict) else cached
|
|
540
|
+
if repaired != cached:
|
|
541
|
+
# An index written before the deterministic repairs existed: fix it in place
|
|
542
|
+
# instead of paying for a new model call.
|
|
543
|
+
_store_index(work_dir, repaired, vlm_analysis)
|
|
516
544
|
log("consolidate(index): understanding_index.json 已最新,跳过")
|
|
517
|
-
return
|
|
545
|
+
return repaired
|
|
546
|
+
_require_api_key("index")
|
|
518
547
|
asr_result = _load(work_dir, "asr_result.json") or []
|
|
519
548
|
asr_clean = _load(work_dir, "asr_clean.json") or {}
|
|
520
549
|
background_research = load_background_research(work_dir)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
"max_tokens": 3000,
|
|
531
|
-
"temperature": 0.2,
|
|
532
|
-
}
|
|
550
|
+
content = _complete_json_reply(
|
|
551
|
+
"index",
|
|
552
|
+
build_index_messages(
|
|
553
|
+
vlm_analysis,
|
|
554
|
+
asr_result=asr_result,
|
|
555
|
+
asr_clean=asr_clean,
|
|
556
|
+
background_research=background_research,
|
|
557
|
+
),
|
|
558
|
+
_INDEX_MAX_TOKENS,
|
|
533
559
|
)
|
|
534
|
-
index = parse_index_response(
|
|
560
|
+
index = parse_index_response(content)
|
|
535
561
|
index = _apply_deterministic_asr_research_fallback(
|
|
536
562
|
index,
|
|
537
563
|
asr_result=asr_result,
|
|
538
564
|
asr_clean=asr_clean,
|
|
539
565
|
background_research=background_research,
|
|
540
566
|
)
|
|
541
|
-
|
|
567
|
+
index = _normalized_index(index, vlm_analysis)
|
|
568
|
+
_store_index(work_dir, index, vlm_analysis)
|
|
569
|
+
log(
|
|
570
|
+
f"consolidate(index): 写出 understanding_index.json(角色 {len(index['characters'])})"
|
|
571
|
+
)
|
|
572
|
+
return index
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _normalized_index(index, vlm_analysis):
|
|
576
|
+
"""Merge split characters and canonicalize plot times (index_normalize)."""
|
|
577
|
+
index, repairs = normalize_index(index, duration=scenes_end(vlm_analysis))
|
|
578
|
+
if repairs["merged_characters"] or repairs["dropped_plot_times"]:
|
|
579
|
+
log(
|
|
580
|
+
f"consolidate(index): 合并重复角色 {repairs['merged_characters']} 个,"
|
|
581
|
+
f"丢弃无效剧情时间 {repairs['dropped_plot_times']} 个"
|
|
582
|
+
)
|
|
583
|
+
return index
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _store_index(work_dir, index, vlm_analysis):
|
|
587
|
+
(work_dir / "understanding_index.json").write_text(
|
|
542
588
|
json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
543
589
|
)
|
|
544
590
|
_write_index_meta(work_dir, vlm_analysis)
|
|
545
591
|
(work_dir / "understanding_index.md").write_text(
|
|
546
592
|
format_index_md(index), encoding="utf-8"
|
|
547
593
|
)
|
|
548
|
-
log(
|
|
549
|
-
f"consolidate(index): 写出 understanding_index.json(角色 {len(index['characters'])})"
|
|
550
|
-
)
|
|
551
|
-
return index
|
|
552
594
|
|
|
553
595
|
|
|
554
596
|
def consolidate(work_dir, do_asr=False, do_index=True):
|
|
555
597
|
"""Default = index-only (Pass B, zero timing risk). Pass A (asr) is opt-in.
|
|
556
598
|
|
|
557
|
-
Failures propagate; understanding_runner catches them and writes the "failed" status.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
599
|
+
Failures propagate; understanding_runner catches them and writes the "failed" status.
|
|
600
|
+
A pass that needs the model while no key is set is listed under "skipped_no_key"."""
|
|
601
|
+
result = {"skipped_no_key": []}
|
|
602
|
+
passes = [("asr", "asr_clean", consolidate_transcript)] if do_asr else []
|
|
561
603
|
if do_index:
|
|
562
|
-
|
|
604
|
+
passes.append(("index", "index", consolidate_index))
|
|
605
|
+
for label, key, run in passes:
|
|
606
|
+
try:
|
|
607
|
+
result[key] = run(work_dir)
|
|
608
|
+
except ConsolidateNoKey:
|
|
609
|
+
result[key] = None
|
|
610
|
+
result["skipped_no_key"].append(label)
|
|
563
611
|
return result
|
|
564
612
|
|
|
565
613
|
|
|
566
614
|
def _response_text(resp):
|
|
615
|
+
"""(content, finish_reason) of the first choice; ("", None) on a malformed response."""
|
|
567
616
|
try:
|
|
568
|
-
|
|
569
|
-
|
|
617
|
+
choice = resp["choices"][0]
|
|
618
|
+
return choice["message"]["content"] or "", choice.get("finish_reason")
|
|
619
|
+
except (KeyError, IndexError, TypeError, AttributeError):
|
|
570
620
|
log("consolidate: API 返回结构异常")
|
|
571
|
-
return ""
|
|
621
|
+
return "", None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _complete_json_reply(label, messages, max_tokens):
|
|
625
|
+
"""Call the model and return a reply that is complete JSON, or raise ConsolidateIncomplete.
|
|
626
|
+
|
|
627
|
+
A cut-off reply (finish_reason=length) is retried once with a larger budget; an
|
|
628
|
+
unparseable reply is not retried (a bigger budget would not fix it)."""
|
|
629
|
+
budget = max_tokens
|
|
630
|
+
for attempt in range(2):
|
|
631
|
+
content, finish = _response_text(
|
|
632
|
+
api_call(
|
|
633
|
+
{
|
|
634
|
+
"model": CONFIG["vlm_model"],
|
|
635
|
+
"messages": messages,
|
|
636
|
+
"max_tokens": budget,
|
|
637
|
+
"temperature": 0.2,
|
|
638
|
+
}
|
|
639
|
+
)
|
|
640
|
+
)
|
|
641
|
+
if finish == "length":
|
|
642
|
+
if attempt == 0:
|
|
643
|
+
log(f"consolidate({label}): 输出被截断(max_tokens={budget}),加大预算重试一次")
|
|
644
|
+
budget *= _RETRY_BUDGET_FACTOR
|
|
645
|
+
continue
|
|
646
|
+
raise ConsolidateIncomplete(
|
|
647
|
+
f"consolidate({label}): 输出被截断(finish_reason=length,"
|
|
648
|
+
f"max_tokens={budget}),未写出结果"
|
|
649
|
+
)
|
|
650
|
+
if not isinstance(_extract_json(content), dict):
|
|
651
|
+
raise ConsolidateIncomplete(
|
|
652
|
+
f"consolidate({label}): 模型返回的不是可解析的 JSON"
|
|
653
|
+
f"(finish_reason={finish}),未写出结果"
|
|
654
|
+
)
|
|
655
|
+
return content
|
|
572
656
|
|
|
573
657
|
|
|
574
658
|
def main():
|
|
@@ -583,7 +667,7 @@ def main():
|
|
|
583
667
|
print(
|
|
584
668
|
json.dumps(
|
|
585
669
|
{
|
|
586
|
-
"status": "consolidated",
|
|
670
|
+
"status": "skipped_no_key" if res["skipped_no_key"] else "consolidated",
|
|
587
671
|
"index": bool(res.get("index")),
|
|
588
672
|
"asr_clean": bool(res.get("asr_clean")),
|
|
589
673
|
},
|