@oh-story/dsh 0.1.12 → 0.1.14
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 +18 -12
- package/lib/client.js +1003 -652
- package/lib/client.js.map +4 -4
- 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 +17 -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/canvas-viewport.d.ts +31 -0
- package/lib/types/client/canvas-viewport.d.ts.map +1 -0
- package/lib/types/client/drama-production-view.d.ts +14 -2
- 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/index.d.ts.map +1 -1
- package/lib/types/client/production-canvas.d.ts +24 -0
- package/lib/types/client/production-canvas.d.ts.map +1 -0
- 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 +123 -1
- package/lib/video-recap/README.en.md +20 -16
- package/lib/video-recap/README.md +20 -16
- 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 +28 -29
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +10 -13
- 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 +55 -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 +96 -117
- 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 +53 -24
- 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 +12 -11
- 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 +51 -18
- 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 +81 -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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Where an interrupting narration block may be told to move.
|
|
2
|
+
|
|
3
|
+
`interrupts_source_sentence` suggests a sentence-end anchor the whole block can shift to.
|
|
4
|
+
These helpers keep that suggestion between the block's neighbours and clear of every other
|
|
5
|
+
block, and decide when a block is a back-to-back handoff that needs no entry check.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_number(value):
|
|
12
|
+
return (
|
|
13
|
+
isinstance(value, (int, float))
|
|
14
|
+
and not isinstance(value, bool)
|
|
15
|
+
and math.isfinite(value)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# A narration block that starts within this gap after another block's end is a
|
|
20
|
+
# back-to-back handoff: the source track is never exposed between the two.
|
|
21
|
+
CONNECTED_HANDOFF_SECONDS = 0.15
|
|
22
|
+
# A suggested entry farther than this from the authored one moves the block off the
|
|
23
|
+
# pictures it was written for; past it the author must rethink the block instead.
|
|
24
|
+
SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS = 10.0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _shift_is_clear(new_start, duration, other_spans):
|
|
28
|
+
"""The block moved whole to new_start neither overlaps nor abuts another block."""
|
|
29
|
+
new_end = new_start + duration
|
|
30
|
+
gap = CONNECTED_HANDOFF_SECONDS
|
|
31
|
+
return all(
|
|
32
|
+
new_end < other_start - gap or new_start > other_end + gap
|
|
33
|
+
for other_start, other_end in other_spans
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _nearest_safe_anchor(start, end, anchors, other_spans, window=(None, None)):
|
|
38
|
+
"""The anchor closest to `start` the whole block can move to (duration kept) without
|
|
39
|
+
overlapping or abutting another block or leaving `window`, within
|
|
40
|
+
SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS; a later anchor wins a tie. None when none qualifies.
|
|
41
|
+
|
|
42
|
+
`window` is the open (low, high) range the moved block must sit strictly inside: the
|
|
43
|
+
neighbours' ends with the handoff gap, so a block never jumps over a neighbour (story
|
|
44
|
+
order) and two adjacent blocks never get colliding suggestions. Either side may be None.
|
|
45
|
+
"""
|
|
46
|
+
duration = end - start
|
|
47
|
+
low, high = window
|
|
48
|
+
candidates = [
|
|
49
|
+
anchor
|
|
50
|
+
for anchor in anchors
|
|
51
|
+
if abs(anchor["time"] - start) <= SUGGESTED_ANCHOR_MAX_SHIFT_SECONDS
|
|
52
|
+
and anchor["time"] >= 0
|
|
53
|
+
and (low is None or anchor["time"] > low)
|
|
54
|
+
and (high is None or anchor["time"] + duration < high)
|
|
55
|
+
and _shift_is_clear(anchor["time"], duration, other_spans)
|
|
56
|
+
]
|
|
57
|
+
if not candidates:
|
|
58
|
+
return None
|
|
59
|
+
return min(candidates, key=lambda anchor: (abs(anchor["time"] - start), -anchor["time"]))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _valid_span(seg):
|
|
63
|
+
"""(start, end) of a block with a usable numeric window, else None."""
|
|
64
|
+
if not isinstance(seg, dict):
|
|
65
|
+
return None
|
|
66
|
+
start, end = seg.get("start"), seg.get("end")
|
|
67
|
+
if _is_number(start) and _is_number(end) and end > start:
|
|
68
|
+
return start, end
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _other_block_spans(narration, idx):
|
|
73
|
+
"""(start, end) of every other block with a usable numeric window."""
|
|
74
|
+
return [
|
|
75
|
+
span
|
|
76
|
+
for other_idx, other in enumerate(narration)
|
|
77
|
+
if other_idx != idx and (span := _valid_span(other))
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Clip/timeline edges are allowed positions; the open window bound sits this far outside.
|
|
82
|
+
_EDGE_EPSILON = 1e-6
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _suggestion_window(narration, idx, suggested_spans, limits=(None, None)):
|
|
86
|
+
"""Open (low, high) range a suggested move of block `idx` must stay strictly inside.
|
|
87
|
+
|
|
88
|
+
Bounded by the previous and next valid blocks (narration is chronological) with the
|
|
89
|
+
handoff gap. The previous block counts at both its authored and its own suggested
|
|
90
|
+
window, so applying any subset of the suggestions keeps adjacent blocks apart and in
|
|
91
|
+
order. `limits` is the closed (first, last) range the block must stay in: its clip in
|
|
92
|
+
cut mode, the output duration in cut_output; either side may be None.
|
|
93
|
+
"""
|
|
94
|
+
gap = CONNECTED_HANDOFF_SECONDS
|
|
95
|
+
low = high = None
|
|
96
|
+
for prev_idx in range(idx - 1, -1, -1):
|
|
97
|
+
span = _valid_span(narration[prev_idx])
|
|
98
|
+
if span:
|
|
99
|
+
low = max(span[1], suggested_spans.get(prev_idx, span)[1]) + gap
|
|
100
|
+
break
|
|
101
|
+
for next_idx in range(idx + 1, len(narration)):
|
|
102
|
+
span = _valid_span(narration[next_idx])
|
|
103
|
+
if span:
|
|
104
|
+
high = span[0] - gap
|
|
105
|
+
break
|
|
106
|
+
first, last = limits
|
|
107
|
+
if first is not None:
|
|
108
|
+
low = max(x for x in (low, first - _EDGE_EPSILON) if x is not None)
|
|
109
|
+
if last is not None:
|
|
110
|
+
high = min(x for x in (high, last + _EDGE_EPSILON) if x is not None)
|
|
111
|
+
return low, high
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _has_connected_predecessor(narration, idx, start):
|
|
115
|
+
"""A back-to-back narration handoff (<=150ms gap) does not expose the source track,
|
|
116
|
+
so the next TTS block is not a new source-speech entry."""
|
|
117
|
+
for other_idx, other in enumerate(narration):
|
|
118
|
+
if other_idx == idx or not isinstance(other, dict):
|
|
119
|
+
continue
|
|
120
|
+
other_start, other_end = other.get("start"), other.get("end")
|
|
121
|
+
if not _is_number(other_start) or not _is_number(other_end):
|
|
122
|
+
continue
|
|
123
|
+
if other_start < start and -0.001 <= start - other_end <= CONNECTED_HANDOFF_SECONDS:
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
@@ -7,13 +7,11 @@ import re
|
|
|
7
7
|
|
|
8
8
|
EVIDENCE_CONTRACT_VERSION = 1
|
|
9
9
|
|
|
10
|
-
COVERAGE_POLICY_VERSION = "coverage_policy_v1"
|
|
11
|
-
|
|
12
10
|
|
|
13
11
|
def _safe_time(item, key, default=0.0):
|
|
14
12
|
try:
|
|
15
13
|
value = float(item.get(key, default))
|
|
16
|
-
except (
|
|
14
|
+
except (TypeError, ValueError):
|
|
17
15
|
return default
|
|
18
16
|
return value if math.isfinite(value) else default
|
|
19
17
|
|
|
@@ -85,7 +83,6 @@ def coverage_policy_v1(
|
|
|
85
83
|
duration = _overall_duration(scenes, asr_result, narration)
|
|
86
84
|
if duration <= 0:
|
|
87
85
|
return {
|
|
88
|
-
"coverage_policy_version": COVERAGE_POLICY_VERSION,
|
|
89
86
|
"selected_ranges": [],
|
|
90
87
|
"dropped_ranges": [],
|
|
91
88
|
"dropped_range_count": 0,
|
|
@@ -162,7 +159,6 @@ def coverage_policy_v1(
|
|
|
162
159
|
for r in merged + dropped:
|
|
163
160
|
r.pop("priority", None)
|
|
164
161
|
return {
|
|
165
|
-
"coverage_policy_version": COVERAGE_POLICY_VERSION,
|
|
166
162
|
"selected_ranges": merged,
|
|
167
163
|
"dropped_ranges": dropped,
|
|
168
164
|
"dropped_range_count": len(dropped),
|
|
@@ -276,13 +272,11 @@ def _research_context_items(research):
|
|
|
276
272
|
|
|
277
273
|
|
|
278
274
|
def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="source"):
|
|
279
|
-
"""Pure
|
|
275
|
+
"""Pure: collect visual/ASR evidence items inside ranges."""
|
|
280
276
|
clock = "output" if timeline == "cut_output" else "source"
|
|
281
277
|
items = []
|
|
282
278
|
dropped = {"visual": 0, "asr": 0}
|
|
283
279
|
for i, scene in enumerate(vlm_analysis or []):
|
|
284
|
-
if not isinstance(scene, dict):
|
|
285
|
-
continue
|
|
286
280
|
start = _safe_time(scene, "start")
|
|
287
281
|
end = _safe_time(scene, "end", start)
|
|
288
282
|
if end <= start or not _in_ranges(start, end, ranges):
|
|
@@ -309,8 +303,6 @@ def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="sou
|
|
|
309
303
|
item[k] = scene.get(k)
|
|
310
304
|
items.append(item)
|
|
311
305
|
for i, seg in enumerate(asr_result or []):
|
|
312
|
-
if not isinstance(seg, dict):
|
|
313
|
-
continue
|
|
314
306
|
text = str(seg.get("text", "")).strip()
|
|
315
307
|
if not text:
|
|
316
308
|
continue
|
|
@@ -346,22 +338,6 @@ def filter_evidence_by_ranges(vlm_analysis, asr_result, ranges, *, timeline="sou
|
|
|
346
338
|
}
|
|
347
339
|
|
|
348
340
|
|
|
349
|
-
def build_review_coverage_metadata(bundle):
|
|
350
|
-
"""Summarize a build_evidence_bundle() bundle's coverage contract."""
|
|
351
|
-
coverage = bundle["coverage"]
|
|
352
|
-
metadata = bundle["metadata"]
|
|
353
|
-
return {
|
|
354
|
-
"coverage_policy_version": coverage["coverage_policy_version"],
|
|
355
|
-
"time_ranges": coverage["selected_ranges"],
|
|
356
|
-
"dropped_ranges": coverage["dropped_ranges"],
|
|
357
|
-
"dropped_range_count": coverage["dropped_range_count"],
|
|
358
|
-
"scene_count": metadata["reviewed_scene_count"],
|
|
359
|
-
"asr_count": metadata["reviewed_asr_count"],
|
|
360
|
-
"dropped_scene_count": metadata["dropped_scene_count"],
|
|
361
|
-
"dropped_asr_count": metadata["dropped_asr_count"],
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
341
|
def build_evidence_bundle(
|
|
366
342
|
vlm_analysis,
|
|
367
343
|
asr_result,
|
|
@@ -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)
|