@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
|
@@ -73,11 +73,6 @@ def _env_number(name, default, cast, minimum):
|
|
|
73
73
|
return value
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def env_int(name, default, *, minimum=None):
|
|
77
|
-
"""Read an integer env var, rejecting malformed or below-minimum values."""
|
|
78
|
-
return _env_number(name, default, int, minimum)
|
|
79
|
-
|
|
80
|
-
|
|
81
76
|
def env_bool(name, default=False):
|
|
82
77
|
"""Read common boolean env var forms."""
|
|
83
78
|
raw = os.environ.get(name)
|
|
@@ -120,9 +115,6 @@ CONFIG = {
|
|
|
120
115
|
"quiet_overlap_min_ratio": 0.8, # 解说段至少多少比例落在安静窗口内才标记为非对白重叠
|
|
121
116
|
"visual_beat_max_seconds": 18.0, # 单段解说超过该时长且跨多个帧锚点时给 lint 提醒
|
|
122
117
|
"visual_beat_max_facts": 3, # 单段解说最多建议覆盖的 frame_facts 锚点数量
|
|
123
|
-
"asr_chunk_min_chars": env_int("ASR_CHUNK_MIN_CHARS", 500, minimum=1), # brief 中 ASR 写作分块最小字数/词数
|
|
124
|
-
"asr_chunk_max_chars": env_int("ASR_CHUNK_MAX_CHARS", 800, minimum=1), # brief 中 ASR 写作分块最大字数/词数
|
|
125
|
-
"edit_mode": os.environ.get("EDIT_MODE", "full"), # full | cut
|
|
126
118
|
}
|
|
127
119
|
|
|
128
120
|
def log(msg):
|
|
@@ -165,9 +157,8 @@ def _sanitize_api_error(value, limit=500):
|
|
|
165
157
|
text = _ERROR_KEY_RE.sub("<redacted-key>", text)
|
|
166
158
|
return text[:limit]
|
|
167
159
|
|
|
168
|
-
def _api_headers(
|
|
160
|
+
def _api_headers(api_key=None):
|
|
169
161
|
"""Build MiMo auth headers (OpenAI-compatible chat/completions with an api-key header)."""
|
|
170
|
-
del api_provider, api_url # MiMo is the only provider; signature kept for call sites
|
|
171
162
|
key = CONFIG["api_key"] if api_key is None else api_key
|
|
172
163
|
return {
|
|
173
164
|
"Content-Type": "application/json",
|
|
@@ -175,9 +166,8 @@ def _api_headers(api_provider=None, api_url=None, api_key=None):
|
|
|
175
166
|
"api-key": key,
|
|
176
167
|
}
|
|
177
168
|
|
|
178
|
-
def _prepare_api_payload(payload
|
|
169
|
+
def _prepare_api_payload(payload):
|
|
179
170
|
"""Normalize payload fields for MiMo's OpenAI-compatible chat/completions API."""
|
|
180
|
-
del api_provider, api_url
|
|
181
171
|
normalized = dict(payload)
|
|
182
172
|
if "max_tokens" in normalized and "max_completion_tokens" not in normalized:
|
|
183
173
|
normalized["max_completion_tokens"] = normalized.pop("max_tokens")
|
|
@@ -192,15 +182,15 @@ def _prepare_api_payload(payload, api_provider=None, api_url=None):
|
|
|
192
182
|
normalized["thinking"] = {"type": "disabled"}
|
|
193
183
|
return normalized
|
|
194
184
|
|
|
195
|
-
def api_call(payload, max_retries=8, *,
|
|
185
|
+
def api_call(payload, max_retries=8, *, api_url=None, api_key=None, api_env_var=None):
|
|
196
186
|
"""调用 OpenAI-compatible API,带重试。
|
|
197
187
|
|
|
198
188
|
集群的 429 限流是常态而非错误,所以重试更耐心(更多次数 + 退避封顶 60s + 遵从 Retry-After),
|
|
199
189
|
避免一次瞬时限流就中止整个阶段。配额窗口常以分钟计,所以 429 在没有 Retry-After 时也至少等 10s。
|
|
200
190
|
"""
|
|
201
191
|
endpoint = normalize_api_url(api_url if api_url is not None else CONFIG["api_url"])
|
|
202
|
-
headers = _api_headers(
|
|
203
|
-
data = json.dumps(_prepare_api_payload(payload
|
|
192
|
+
headers = _api_headers(api_key=api_key)
|
|
193
|
+
data = json.dumps(_prepare_api_payload(payload)).encode("utf-8")
|
|
204
194
|
|
|
205
195
|
for attempt in range(max_retries):
|
|
206
196
|
try:
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Render a narration lint report as the compact lines the author reads on the console.
|
|
2
|
+
|
|
3
|
+
narration_lint.json keeps every field; this summary repeats, per flagged block, the
|
|
4
|
+
numbers needed to fix it without opening that file. Errors (failed lint) and warnings
|
|
5
|
+
(passed lint) use the same line format. Block numbers are 1-based ("段 N",
|
|
6
|
+
the block's position in narration.json); the report's `index` fields stay 0-based. A
|
|
7
|
+
deslop blocker found in original_subtitles.json carries `source: "original_subtitles"`
|
|
8
|
+
and an index into that file, so it is labelled "原声字幕第 N 条", never "段 N".
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
MAX_LISTED_ERRORS = 12
|
|
12
|
+
|
|
13
|
+
_HINTS = {
|
|
14
|
+
"over_budget": "缩短文字,或放宽/挪动时间窗",
|
|
15
|
+
"interrupts_source_sentence": "把整块(时长不变)挪到建议的时间窗;没有建议入点时缩短、挪走或删掉这块",
|
|
16
|
+
"source_sentence_anchors_unavailable": "挪走或删掉这块,或重新生成含句尾锚点的语音证据",
|
|
17
|
+
"time_overlap": "调整时间窗,让两块不重叠",
|
|
18
|
+
"out_of_order": "按 start 从小到大排列各块",
|
|
19
|
+
"outside_clip_plan": "把这块挪进某个选中片段的时间范围",
|
|
20
|
+
"ambiguous_source_clip": "给这块写 source_clip_id",
|
|
21
|
+
"em_dash": "删掉破折号,改用逗号、句号或把句子拆开",
|
|
22
|
+
"placeholder_leakage": "把示例占位(如【主角】)换成真实人名或内容",
|
|
23
|
+
"slot_too_short": "放宽时间窗,或把这块并进相邻块",
|
|
24
|
+
"incomplete_sentence": "用句号、问号或感叹号收尾",
|
|
25
|
+
"outside_scene": "核对时间,确认这块对应哪段画面",
|
|
26
|
+
"crosses_scene_boundary": "把这块收进所在场景,或在场景切换处拆成两块",
|
|
27
|
+
"visual_beat_too_broad": "拆块或收紧时间窗,让旁白贴着当前画面",
|
|
28
|
+
"crosses_clip_boundary": "把这块收进所在片段,超出部分会被裁掉",
|
|
29
|
+
"no_original_blocks": "在几个强原声时刻不写旁白,让原声满音量播放",
|
|
30
|
+
"under_narrated": "确认每个长空档都有意交给原声;只在有明确解说任务时加块",
|
|
31
|
+
"no_original_breaks": "在块之间留几个多秒的空档给原声",
|
|
32
|
+
"fragmented_beats": "把相邻、讲同一件事的短句并成一块",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
_ORIGINAL_SUBTITLE_HINTS = {
|
|
36
|
+
"em_dash": "在 original_subtitles.json 里删掉这条的破折号(不是改 narration.json)",
|
|
37
|
+
"placeholder_leakage": "在 original_subtitles.json 里替换这条的示例占位(不是改 narration.json)",
|
|
38
|
+
}
|
|
39
|
+
_ORIGINAL_SUBTITLE_FALLBACK_HINT = "改 original_subtitles.json 里的这一条,不是 narration.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def block_label(index):
|
|
43
|
+
"""'段 N' for a 0-based block index; '整体' for a file-level issue."""
|
|
44
|
+
return "整体" if index is None else f"段 {index + 1}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _label(e):
|
|
48
|
+
if e.get("source") == "original_subtitles":
|
|
49
|
+
index = e.get("index")
|
|
50
|
+
return "原声字幕" if index is None else f"原声字幕第 {index + 1} 条"
|
|
51
|
+
return block_label(e.get("index"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _hint(e):
|
|
55
|
+
code = e.get("code")
|
|
56
|
+
if e.get("source") == "original_subtitles":
|
|
57
|
+
return _ORIGINAL_SUBTITLE_HINTS.get(code, _ORIGINAL_SUBTITLE_FALLBACK_HINT)
|
|
58
|
+
return _HINTS.get(code)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _seconds(value):
|
|
62
|
+
return f"{value:.2f}s" if isinstance(value, (int, float)) else "?"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _window(e):
|
|
66
|
+
return f"{_seconds(e.get('start'))}-{_seconds(e.get('end'))}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _over_budget(e):
|
|
70
|
+
head = f"{_window(e)} 写了 {e.get('actual_chars')} 字,窗口约 {e.get('budget_chars')} 字"
|
|
71
|
+
if "limit_chars" in e: # the full-mode error
|
|
72
|
+
return f"{head}(硬上限 {e.get('limit_chars')},超出 {e.get('over_chars')} 字)"
|
|
73
|
+
return (
|
|
74
|
+
f"{head},估计读完 {_seconds(e.get('estimated_tts_seconds'))},"
|
|
75
|
+
f"可用 {_seconds(e.get('slot_seconds'))}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _interrupts(e):
|
|
80
|
+
text = f"入点 {_seconds(e.get('entry_time'))} 落在原声句子中间"
|
|
81
|
+
if e.get("suggested_start") is None:
|
|
82
|
+
shift = e.get("max_shift_seconds")
|
|
83
|
+
within = f"前后 {shift:g} 秒内" if isinstance(shift, (int, float)) else "附近"
|
|
84
|
+
return text + f",{within}没有能整块挪过去、留在前后两块之间又不与其他块重叠或相接的句尾锚点"
|
|
85
|
+
tail = e.get("source_text_tail")
|
|
86
|
+
tail = f",句尾「{tail}」" if tail else ""
|
|
87
|
+
target = _seconds(e["suggested_start"])
|
|
88
|
+
if e.get("suggested_end") is not None:
|
|
89
|
+
target += f"-{_seconds(e['suggested_end'])}"
|
|
90
|
+
return (
|
|
91
|
+
f"{text},建议入点 {target}"
|
|
92
|
+
f"(锚点 {e.get('anchor_confidence')}/{e.get('anchor_boundary_use')}{tail})"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _ratio(value):
|
|
97
|
+
return f"{value:.0%}" if isinstance(value, (int, float)) else "?"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_WARNING_DETAILS = {
|
|
101
|
+
"slot_too_short": lambda e: f"{_window(e)} 只容得下约 {e.get('budget_chars')} 字,TTS 可能放不下",
|
|
102
|
+
"incomplete_sentence": lambda e: f"结尾「{e.get('text_tail', '')}」没有句末标点",
|
|
103
|
+
"outside_scene": lambda e: f"{_window(e)} 的中点不在任何检测到的场景里",
|
|
104
|
+
"crosses_scene_boundary": lambda e: (
|
|
105
|
+
f"{_window(e)} 超出所在场景 "
|
|
106
|
+
f"{_seconds(e.get('scene_start'))}-{_seconds(e.get('scene_end'))}"
|
|
107
|
+
),
|
|
108
|
+
"visual_beat_too_broad": lambda e: (
|
|
109
|
+
f"{_window(e)} 共 {_seconds(e.get('duration'))},"
|
|
110
|
+
f"跨过 {e.get('frame_fact_count', '?')} 个画面锚点"
|
|
111
|
+
),
|
|
112
|
+
"crosses_clip_boundary": lambda e: (
|
|
113
|
+
f"{_window(e)} 超出所在片段 {_seconds(e.get('clip_start'))}-{_seconds(e.get('clip_end'))}"
|
|
114
|
+
),
|
|
115
|
+
"no_original_blocks": lambda e: (
|
|
116
|
+
f"旁白覆盖率 {_ratio(e.get('narration_coverage'))},超过上限 {_ratio(e.get('coverage_max'))}"
|
|
117
|
+
),
|
|
118
|
+
"under_narrated": lambda e: (
|
|
119
|
+
f"旁白覆盖率 {_ratio(e.get('narration_coverage'))},低于下限 {_ratio(e.get('coverage_min'))}"
|
|
120
|
+
),
|
|
121
|
+
"no_original_breaks": lambda e: (
|
|
122
|
+
f"块与块之间没有 ≥{_seconds(e.get('original_block_min_seconds'))} 的原声段"
|
|
123
|
+
),
|
|
124
|
+
"fragmented_beats": lambda e: (
|
|
125
|
+
f"平均每块 {e.get('avg_block_chars')} 字,少于 {e.get('block_min_chars')} 字"
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _details(e):
|
|
131
|
+
code = e.get("code")
|
|
132
|
+
if code == "over_budget" and "budget_chars" in e:
|
|
133
|
+
return _over_budget(e)
|
|
134
|
+
if code == "interrupts_source_sentence":
|
|
135
|
+
return _interrupts(e)
|
|
136
|
+
if code == "source_sentence_anchors_unavailable":
|
|
137
|
+
return f"入点 {_seconds(e.get('entry_time'))} 压在原声上,但没有已验证的句尾锚点"
|
|
138
|
+
if code == "time_overlap":
|
|
139
|
+
return (
|
|
140
|
+
f"{_seconds(e.get('start'))}-{_seconds(e.get('end'))} 与"
|
|
141
|
+
f"{block_label(e.get('previous_index'))}(结束于 {_seconds(e.get('previous_end'))})重叠"
|
|
142
|
+
)
|
|
143
|
+
if code == "out_of_order":
|
|
144
|
+
return f"start {_seconds(e.get('start'))} 早于上一块的 {_seconds(e.get('previous_start'))}"
|
|
145
|
+
if code in _WARNING_DETAILS:
|
|
146
|
+
return _WARNING_DETAILS[code](e)
|
|
147
|
+
return str(e.get("message", "")).strip()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _issue_line(e):
|
|
151
|
+
line = f"- {_label(e)} {e.get('code')}:{_details(e)}"
|
|
152
|
+
hint = _hint(e)
|
|
153
|
+
if hint:
|
|
154
|
+
line += f"。改法:{hint}"
|
|
155
|
+
return line
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _warning_lines(report):
|
|
159
|
+
"""One line per warning, every warning listed: they never block, so none is cut."""
|
|
160
|
+
return [_issue_line(w) for w in report.get("warnings", [])]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def format_lint_warnings(report, lint_path=None):
|
|
164
|
+
"""The console summary of a lint that passed with warnings."""
|
|
165
|
+
lines = [f"narration lint:通过,{len(report.get('warnings', []))} 个 warning(不阻塞)"]
|
|
166
|
+
lines.extend(_warning_lines(report))
|
|
167
|
+
if lint_path is not None:
|
|
168
|
+
lines.append(f"完整报告:{lint_path}")
|
|
169
|
+
return "\n".join(lines)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def format_lint_failure(report, lint_path=None):
|
|
173
|
+
errors = report.get("errors", [])
|
|
174
|
+
lines = [f"narration.json 预检失败:{len(errors)} 个 error,修改后重跑 validate。"]
|
|
175
|
+
lines.extend(_issue_line(e) for e in errors[:MAX_LISTED_ERRORS])
|
|
176
|
+
if len(errors) > MAX_LISTED_ERRORS:
|
|
177
|
+
lines.append(f"- …另有 {len(errors) - MAX_LISTED_ERRORS} 个 error 未列出")
|
|
178
|
+
warnings = _warning_lines(report)
|
|
179
|
+
if warnings:
|
|
180
|
+
lines.append(f"另有 {len(warnings)} 个 warning(不阻塞):")
|
|
181
|
+
lines.extend(warnings)
|
|
182
|
+
if lint_path is not None:
|
|
183
|
+
lines.append(f"完整报告(含全部字段):{lint_path}")
|
|
184
|
+
return "\n".join(lines)
|
|
@@ -5,21 +5,26 @@ timing and text checks live here, and every later consumer trusts its result.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
import math
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
|
|
11
10
|
from lib import CONFIG, log
|
|
12
11
|
from agent_text import (
|
|
13
|
-
|
|
12
|
+
TTS_UTTERANCE_OVERHEAD_SECONDS,
|
|
14
13
|
_find_scene_for_midpoint,
|
|
15
|
-
|
|
16
|
-
_post_dedup_narration,
|
|
17
|
-
_recommended_char_budget,
|
|
14
|
+
_lint_char_budget,
|
|
18
15
|
_scene_available_seconds,
|
|
19
16
|
_text_char_count,
|
|
20
|
-
_truncate_at_sentence,
|
|
21
17
|
)
|
|
22
18
|
from deslop_qc import analyze_deslop_qc
|
|
19
|
+
from entry_suggestion import (
|
|
20
|
+
SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS,
|
|
21
|
+
_has_connected_predecessor,
|
|
22
|
+
_is_number,
|
|
23
|
+
_nearest_safe_anchor,
|
|
24
|
+
_other_block_spans,
|
|
25
|
+
_suggestion_window,
|
|
26
|
+
)
|
|
27
|
+
from lint_summary import format_lint_failure, format_lint_warnings
|
|
23
28
|
from speech_ownership import (
|
|
24
29
|
entry_overlaps_source_speech,
|
|
25
30
|
load_source_sentence_evidence,
|
|
@@ -32,14 +37,6 @@ def _lint_issue(level, index, code, message, **extra):
|
|
|
32
37
|
return issue
|
|
33
38
|
|
|
34
39
|
|
|
35
|
-
def _is_number(value):
|
|
36
|
-
return (
|
|
37
|
-
isinstance(value, (int, float))
|
|
38
|
-
and not isinstance(value, bool)
|
|
39
|
-
and math.isfinite(value)
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
40
|
def _visual_overlay_issues(index, seg):
|
|
44
41
|
"""Shape-check the renderer handoff recap reads verbatim from a validated segment.
|
|
45
42
|
|
|
@@ -134,11 +131,16 @@ def _clip_matches_for_segment(seg, clip_plan, requested_clip_id):
|
|
|
134
131
|
]
|
|
135
132
|
|
|
136
133
|
|
|
137
|
-
def _source_sentence_entry_issue(
|
|
134
|
+
def _source_sentence_entry_issue(
|
|
135
|
+
index, start, end, anchors, speech_owned, other_spans=(), window=(None, None),
|
|
136
|
+
):
|
|
138
137
|
"""Return a blocking issue when narration enters midway through source speech.
|
|
139
138
|
|
|
140
139
|
The validator reports a correction instead of silently moving audio. Source
|
|
141
140
|
sentence integrity is invariant; an editorial policy string cannot bypass it.
|
|
141
|
+
`other_spans` are the other blocks' (start, end) and `window` the open range between
|
|
142
|
+
the neighbours (see _nearest_safe_anchor): the suggested entry never moves this block
|
|
143
|
+
onto, flush against, or past one of them.
|
|
142
144
|
"""
|
|
143
145
|
if not speech_owned:
|
|
144
146
|
return None
|
|
@@ -165,45 +167,81 @@ def _source_sentence_entry_issue(index, start, anchors, speech_owned):
|
|
|
165
167
|
if pause_start - 0.05 <= start <= when + 0.08:
|
|
166
168
|
return None
|
|
167
169
|
|
|
168
|
-
suggested =
|
|
169
|
-
(anchor for anchor in anchors if anchor["time"] > start + 0.08), None
|
|
170
|
-
)
|
|
170
|
+
suggested = _nearest_safe_anchor(start, end, anchors, other_spans, window)
|
|
171
171
|
return _lint_issue(
|
|
172
172
|
"error",
|
|
173
173
|
index,
|
|
174
174
|
"interrupts_source_sentence",
|
|
175
|
-
"Narration enters before the source sentence finishes. Move
|
|
176
|
-
"sentence-end anchor, or shorten/move/remove it when
|
|
177
|
-
"
|
|
175
|
+
"Narration enters before the source sentence finishes. Move the whole block (same "
|
|
176
|
+
"duration) to the suggested sentence-end anchor, or shorten/move/remove it when no "
|
|
177
|
+
f"anchor within {SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS:g}s keeps it between its neighbours "
|
|
178
|
+
"and clear of the other blocks, then rerun lint before TTS. The window stays inside the "
|
|
179
|
+
"block's clip (cut) or the output duration (cut_output); full mode does not know the "
|
|
180
|
+
"video's end, so keep it inside the video. Source sentence interruption has no override.",
|
|
178
181
|
entry_time=round(start, 3),
|
|
179
182
|
suggested_start=round(suggested["time"], 3) if suggested else None,
|
|
183
|
+
suggested_end=round(suggested["time"] + end - start, 3) if suggested else None,
|
|
184
|
+
max_shift_seconds=SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS,
|
|
180
185
|
source_text_tail=str(suggested.get("text_tail", "")).strip() if suggested else "",
|
|
181
186
|
anchor_confidence=suggested["confidence"] if suggested else None,
|
|
187
|
+
anchor_boundary_use=(
|
|
188
|
+
suggested.get("boundary_use")
|
|
189
|
+
or ("unverified" if suggested["confidence"] in {"high", "medium"} else "none")
|
|
190
|
+
)
|
|
191
|
+
if suggested
|
|
192
|
+
else None,
|
|
182
193
|
)
|
|
183
194
|
|
|
184
195
|
|
|
185
|
-
def
|
|
186
|
-
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
def _suggestion_limits(seg, clip_plan, mode, output_duration):
|
|
197
|
+
"""Closed range a suggested move must stay in: the block's one clip (cut mode, source
|
|
198
|
+
time), or [0, output_duration] when that is known (cut_output)."""
|
|
199
|
+
if mode == "cut" and clip_plan:
|
|
200
|
+
requested = seg.get("source_clip_id")
|
|
201
|
+
try:
|
|
202
|
+
requested = None if requested is None else int(requested)
|
|
203
|
+
except (TypeError, ValueError):
|
|
204
|
+
return None, None
|
|
205
|
+
matches = _clip_matches_for_segment(seg, clip_plan, requested)
|
|
206
|
+
return _clip_span(matches[0]) if len(matches) == 1 else (None, None)
|
|
207
|
+
usable = _is_number(output_duration) and output_duration > 0
|
|
208
|
+
return 0.0, output_duration if usable else None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# Full-mode hard text budget: a segment longer than this multiple of its recommended
|
|
212
|
+
# character budget cannot be spoken in its window, so lint returns it to the author
|
|
213
|
+
# instead of anything downstream shortening it.
|
|
214
|
+
OVER_BUDGET_ERROR_RATIO = 1.25
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _over_budget_error(index, start, end, char_count, budget):
|
|
218
|
+
limit = int(budget * OVER_BUDGET_ERROR_RATIO)
|
|
219
|
+
return _lint_issue(
|
|
220
|
+
"error",
|
|
221
|
+
index,
|
|
222
|
+
"over_budget",
|
|
223
|
+
f"段 {index + 1} [{start:.2f}-{end:.2f}s] has {char_count} chars; the window holds "
|
|
224
|
+
f"about {budget} (hard limit {limit}), {char_count - limit} over the limit. "
|
|
225
|
+
"Shorten the text or widen/move the window, then rerun validate; "
|
|
226
|
+
"validation never shortens approved text.",
|
|
227
|
+
start=start,
|
|
228
|
+
end=end,
|
|
229
|
+
budget_chars=budget,
|
|
230
|
+
limit_chars=limit,
|
|
231
|
+
actual_chars=char_count,
|
|
232
|
+
over_chars=char_count - limit,
|
|
233
|
+
tts_overhead_seconds=TTS_UTTERANCE_OVERHEAD_SECONDS,
|
|
234
|
+
)
|
|
197
235
|
|
|
198
236
|
|
|
199
237
|
def lint_narration(
|
|
200
238
|
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
|
|
201
|
-
|
|
239
|
+
output_duration=None,
|
|
202
240
|
):
|
|
203
241
|
"""Preflight-check agent narration before TTS; write narration_lint.json when work_dir is set.
|
|
204
242
|
|
|
205
|
-
Segments
|
|
206
|
-
|
|
243
|
+
Segments must already be in chronological order; validation never reorders them.
|
|
244
|
+
`output_duration` (cut_output) bounds where interrupt suggestions may move a block."""
|
|
207
245
|
scenes_analysis = scenes_analysis or []
|
|
208
246
|
errors = []
|
|
209
247
|
warnings = []
|
|
@@ -221,6 +259,8 @@ def lint_narration(
|
|
|
221
259
|
)
|
|
222
260
|
else:
|
|
223
261
|
previous_start = None
|
|
262
|
+
# Suggested (start, end) of blocks already told to move; later blocks keep clear.
|
|
263
|
+
suggested_spans = {}
|
|
224
264
|
for idx, seg in enumerate(narration):
|
|
225
265
|
if not isinstance(seg, dict):
|
|
226
266
|
errors.append(
|
|
@@ -240,7 +280,7 @@ def lint_narration(
|
|
|
240
280
|
)
|
|
241
281
|
)
|
|
242
282
|
continue
|
|
243
|
-
if
|
|
283
|
+
if previous_start is not None and start < previous_start:
|
|
244
284
|
errors.append(
|
|
245
285
|
_lint_issue(
|
|
246
286
|
"error",
|
|
@@ -304,12 +344,18 @@ def lint_narration(
|
|
|
304
344
|
continue
|
|
305
345
|
|
|
306
346
|
char_count = _text_char_count(text)
|
|
307
|
-
budget =
|
|
347
|
+
budget = _lint_char_budget(start, end)
|
|
308
348
|
# estimate at the REAL playback rate (after the narration_speed atempo); otherwise a
|
|
309
|
-
# beat sized to its 1.3x-sped slot looks "over budget" when it actually fits.
|
|
349
|
+
# beat sized to its 1.3x-sped slot looks "over budget" when it actually fits. The
|
|
350
|
+
# utterance's TTS edge silence takes room too.
|
|
310
351
|
play_rate = CONFIG["speech_rate"] * CONFIG["narration_speed"]
|
|
311
|
-
estimated_tts_seconds = char_count / play_rate
|
|
352
|
+
estimated_tts_seconds = char_count / play_rate + TTS_UTTERANCE_OVERHEAD_SECONDS
|
|
312
353
|
slot_seconds = _scene_available_seconds(start, end)
|
|
354
|
+
over_limit = (
|
|
355
|
+
mode == "full" and char_count > budget * OVER_BUDGET_ERROR_RATIO
|
|
356
|
+
)
|
|
357
|
+
if over_limit:
|
|
358
|
+
errors.append(_over_budget_error(idx, start, end, char_count, budget))
|
|
313
359
|
if budget < 5:
|
|
314
360
|
warnings.append(
|
|
315
361
|
_lint_issue(
|
|
@@ -322,7 +368,9 @@ def lint_narration(
|
|
|
322
368
|
budget_chars=budget,
|
|
323
369
|
)
|
|
324
370
|
)
|
|
325
|
-
|
|
371
|
+
# Independent of slot_too_short: a short slot can also be overfilled, and the
|
|
372
|
+
# author needs both numbers.
|
|
373
|
+
if not over_limit and estimated_tts_seconds > slot_seconds:
|
|
326
374
|
warnings.append(
|
|
327
375
|
_lint_issue(
|
|
328
376
|
"warning",
|
|
@@ -352,15 +400,25 @@ def lint_narration(
|
|
|
352
400
|
entry_issue = _source_sentence_entry_issue(
|
|
353
401
|
idx,
|
|
354
402
|
start,
|
|
403
|
+
end,
|
|
355
404
|
source_sentence_anchors,
|
|
356
405
|
entry_overlaps_source_speech(
|
|
357
406
|
start,
|
|
358
407
|
source_evidence,
|
|
359
408
|
authored_overlap=seg.get("overlaps_speech", True),
|
|
360
409
|
),
|
|
410
|
+
_other_block_spans(narration, idx),
|
|
411
|
+
_suggestion_window(
|
|
412
|
+
narration, idx, suggested_spans,
|
|
413
|
+
_suggestion_limits(seg, clip_plan, mode, output_duration),
|
|
414
|
+
),
|
|
361
415
|
)
|
|
362
416
|
if entry_issue:
|
|
363
417
|
errors.append(entry_issue)
|
|
418
|
+
if entry_issue.get("suggested_end") is not None:
|
|
419
|
+
suggested_spans[idx] = (
|
|
420
|
+
entry_issue["suggested_start"], entry_issue["suggested_end"],
|
|
421
|
+
)
|
|
364
422
|
|
|
365
423
|
scene_bounds = _scene_bounds_for_midpoint(scenes_analysis, start, end)
|
|
366
424
|
if scenes_analysis and not scene_bounds:
|
|
@@ -404,6 +462,7 @@ def lint_narration(
|
|
|
404
462
|
end=end,
|
|
405
463
|
duration=round(end - start, 2),
|
|
406
464
|
frame_fact_times=[round(ts, 2) for ts in frame_fact_times[:8]],
|
|
465
|
+
frame_fact_count=len(frame_fact_times),
|
|
407
466
|
)
|
|
408
467
|
)
|
|
409
468
|
|
|
@@ -603,11 +662,9 @@ def lint_narration(
|
|
|
603
662
|
)
|
|
604
663
|
)
|
|
605
664
|
|
|
665
|
+
# Unfiltered, so narration-sourced blocker indices are narration.json positions.
|
|
606
666
|
deslop_qc = analyze_deslop_qc(
|
|
607
|
-
|
|
608
|
-
if isinstance(narration, list)
|
|
609
|
-
else [],
|
|
610
|
-
work_dir=work_dir,
|
|
667
|
+
narration if isinstance(narration, list) else [], work_dir=work_dir,
|
|
611
668
|
)
|
|
612
669
|
for blocker in deslop_qc["blockers"]:
|
|
613
670
|
errors.append(
|
|
@@ -632,76 +689,39 @@ def lint_narration(
|
|
|
632
689
|
"warnings": warnings,
|
|
633
690
|
}
|
|
634
691
|
if work_dir is not None:
|
|
635
|
-
Path(work_dir, "deslop_qc.json").write_text(
|
|
636
|
-
json.dumps(deslop_qc, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
637
|
-
)
|
|
638
692
|
Path(work_dir, "narration_lint.json").write_text(
|
|
639
693
|
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
640
694
|
)
|
|
641
695
|
return report
|
|
642
696
|
|
|
643
697
|
|
|
698
|
+
class NarrationLintError(ValueError):
|
|
699
|
+
"""Lint found blocking errors: an ordinary result for the author to fix, not a crash.
|
|
700
|
+
|
|
701
|
+
str() is the compact per-block summary; `report` is the full lint report."""
|
|
702
|
+
|
|
703
|
+
def __init__(self, report, lint_path=None):
|
|
704
|
+
self.report = report
|
|
705
|
+
super().__init__(format_lint_failure(report, lint_path))
|
|
706
|
+
|
|
707
|
+
|
|
644
708
|
def validate_narration_or_raise(
|
|
645
709
|
narration, scenes_analysis=None, *, clip_plan=None, mode="full", work_dir=None,
|
|
646
|
-
|
|
710
|
+
output_duration=None,
|
|
647
711
|
):
|
|
648
712
|
report = lint_narration(
|
|
649
713
|
narration, scenes_analysis, clip_plan=clip_plan, mode=mode, work_dir=work_dir,
|
|
650
|
-
|
|
714
|
+
output_duration=output_duration,
|
|
651
715
|
)
|
|
652
716
|
if report["errors"]:
|
|
653
|
-
|
|
654
|
-
|
|
717
|
+
raise NarrationLintError(
|
|
718
|
+
report, None if work_dir is None else Path(work_dir, "narration_lint.json")
|
|
655
719
|
)
|
|
656
|
-
raise ValueError(f"narration.json 预检失败: {sample}; 详见 narration_lint.json")
|
|
657
720
|
if report["warnings"]:
|
|
658
|
-
log(
|
|
659
|
-
|
|
660
|
-
)
|
|
721
|
+
log(format_lint_warnings(
|
|
722
|
+
report, None if work_dir is None else Path(work_dir, "narration_lint.json")
|
|
723
|
+
))
|
|
661
724
|
else:
|
|
662
|
-
log("narration lint
|
|
725
|
+
log("narration lint:通过")
|
|
663
726
|
return report
|
|
664
727
|
|
|
665
|
-
|
|
666
|
-
def _validate_narration_budget(narration, scenes_analysis):
|
|
667
|
-
"""Trim lint-validated narration to its timing budgets; drop what cannot be spoken."""
|
|
668
|
-
del scenes_analysis # scene boundaries are advisory (lint warns); authored timing is kept
|
|
669
|
-
cleaned = []
|
|
670
|
-
for raw in narration:
|
|
671
|
-
item = _normalise_narration_segment(raw)
|
|
672
|
-
max_chars = _recommended_char_budget(item["start"], item["end"])
|
|
673
|
-
if max_chars < 5:
|
|
674
|
-
log(f" 丢弃过短解说段 {item['start']:.1f}-{item['end']:.1f}s")
|
|
675
|
-
continue
|
|
676
|
-
if _text_char_count(item["narration"]) > max_chars * 1.25:
|
|
677
|
-
truncated = _truncate_at_sentence(item["narration"], max_chars)
|
|
678
|
-
if truncated and _text_char_count(truncated) >= 5:
|
|
679
|
-
log(f" 解说超预算,已截短: {item['start']:.1f}-{item['end']:.1f}s")
|
|
680
|
-
item["narration"] = truncated
|
|
681
|
-
else:
|
|
682
|
-
log(
|
|
683
|
-
f" 解说超预算且无法安全截断,已丢弃: {item['start']:.1f}-{item['end']:.1f}s"
|
|
684
|
-
)
|
|
685
|
-
continue
|
|
686
|
-
item["narration"] = _clean_narration_punctuation(item["narration"])
|
|
687
|
-
stripped = item["narration"].strip()
|
|
688
|
-
if stripped and stripped[-1] in ",:、;,—":
|
|
689
|
-
item["narration"] = stripped.rstrip(",:、;,—") + "。"
|
|
690
|
-
cleaned.append(item)
|
|
691
|
-
|
|
692
|
-
cleaned.sort(key=lambda n: n["start"])
|
|
693
|
-
deduped = []
|
|
694
|
-
for item in cleaned:
|
|
695
|
-
if deduped and item["start"] < deduped[-1]["end"]:
|
|
696
|
-
prev = deduped[-1]
|
|
697
|
-
log(
|
|
698
|
-
f" 解说时间重叠: {item['start']:.1f}-{item['end']:.1f}s vs "
|
|
699
|
-
f"{prev['start']:.1f}-{prev['end']:.1f}s"
|
|
700
|
-
)
|
|
701
|
-
if _text_char_count(item["narration"]) > _text_char_count(
|
|
702
|
-
prev["narration"]
|
|
703
|
-
):
|
|
704
|
-
deduped[-1] = item
|
|
705
|
-
else:
|
|
706
|
-
deduped.append(item)
|
|
707
|
-
return _post_dedup_narration(deduped)
|
|
@@ -1,40 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""CLI entrypoint for narration review."""
|
|
3
3
|
|
|
4
|
-
from
|
|
5
|
-
build_evidence_bundle,
|
|
6
|
-
build_review_coverage_metadata,
|
|
7
|
-
coverage_policy_v1,
|
|
8
|
-
filter_evidence_by_ranges,
|
|
9
|
-
render_evidence_bundle,
|
|
10
|
-
)
|
|
11
|
-
from review_grounding import remap_grounding_to_output_timeline
|
|
12
|
-
from review_response import (
|
|
13
|
-
build_grounding_qc,
|
|
14
|
-
build_review_messages,
|
|
15
|
-
format_review_md,
|
|
16
|
-
merge_review_findings,
|
|
17
|
-
parse_review_response,
|
|
18
|
-
write_grounding_qc,
|
|
19
|
-
)
|
|
20
|
-
from review_runner import main, review_narration
|
|
4
|
+
from review_runner import main
|
|
21
5
|
|
|
22
|
-
__all__ = [
|
|
23
|
-
"build_evidence_bundle",
|
|
24
|
-
"build_grounding_qc",
|
|
25
|
-
"build_review_coverage_metadata",
|
|
26
|
-
"build_review_messages",
|
|
27
|
-
"coverage_policy_v1",
|
|
28
|
-
"filter_evidence_by_ranges",
|
|
29
|
-
"format_review_md",
|
|
30
|
-
"main",
|
|
31
|
-
"merge_review_findings",
|
|
32
|
-
"parse_review_response",
|
|
33
|
-
"remap_grounding_to_output_timeline",
|
|
34
|
-
"render_evidence_bundle",
|
|
35
|
-
"review_narration",
|
|
36
|
-
"write_grounding_qc",
|
|
37
|
-
]
|
|
6
|
+
__all__ = ["main"]
|
|
38
7
|
|
|
39
8
|
if __name__ == "__main__":
|
|
40
9
|
main()
|