@oh-story/dsh 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/lib/client.js +85 -35
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +41 -36
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
- package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
- package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
- package/lib/index.js +16 -7
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/README.md +98 -44
- package/lib/novel-to-game/README_ZH.md +100 -46
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
- package/lib/novel-to-game/manifest.json +303 -78
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
- package/lib/types/client/drama-production-view.d.ts +11 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +1 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +7 -2
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +108 -1
- package/lib/video-recap/README.en.md +18 -14
- package/lib/video-recap/README.md +18 -14
- package/lib/video-recap/manifest.json +297 -321
- package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
- package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
- package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
- package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
- package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
- package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
- package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
- package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
- package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
- package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
- package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
- package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
- package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
- package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
- package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
- package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
- package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
- package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
- package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
- package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
- package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
- package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
- package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
- package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
- package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
- package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
- package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
- package/lib/video-recap/skills/video-script/SKILL.md +9 -7
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
- package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
- package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
- package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
- package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
- package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
- package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
- package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
- package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
- package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
- package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
- package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
- package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
- package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
- package/package.json +45 -45
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
- /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
"""Shared shift-left QC report contract for video-recap artifacts.
|
|
2
|
-
|
|
3
|
-
This module is intentionally standalone: it defines a small schema and local
|
|
4
|
-
validation helpers only. It does not call MiMo, read credentials, connect the
|
|
5
|
-
pipeline, or attempt automatic fixes.
|
|
6
|
-
"""
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
from collections.abc import Mapping, Sequence
|
|
13
|
-
from urllib.parse import urlsplit, urlunsplit
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
SCHEMA_VERSION = 1
|
|
17
|
-
|
|
18
|
-
STAGES = frozenset({
|
|
19
|
-
"pre_cut",
|
|
20
|
-
"post_cut",
|
|
21
|
-
"pre_tts",
|
|
22
|
-
"post_tts",
|
|
23
|
-
"pre_assemble",
|
|
24
|
-
"post_render",
|
|
25
|
-
"golden",
|
|
26
|
-
})
|
|
27
|
-
SEVERITIES = frozenset({"info", "advisory", "warning", "blocker"})
|
|
28
|
-
CONFIDENCES = frozenset({"low", "medium", "high", "objective"})
|
|
29
|
-
SAMPLE_POLICIES = frozenset({"all", "deterministic", "sampled", "semantic", "aesthetic"})
|
|
30
|
-
ARTIFACTS = frozenset({"final_qc.json", "golden_eval.json", "mimo_qc.json", "preflight_qc.json"})
|
|
31
|
-
|
|
32
|
-
DETERMINISTIC_CATEGORIES = frozenset({
|
|
33
|
-
"missing_artifact",
|
|
34
|
-
"duration",
|
|
35
|
-
"stream",
|
|
36
|
-
"subtitle",
|
|
37
|
-
"tts_placement",
|
|
38
|
-
"schema_invalid",
|
|
39
|
-
"placement",
|
|
40
|
-
})
|
|
41
|
-
NON_DETERMINISTIC_CATEGORIES = frozenset({"semantic", "aesthetic", "mimo_semantic", "mimo_aesthetic"})
|
|
42
|
-
BLOCKING_SEVERITIES = frozenset({"blocker"})
|
|
43
|
-
_SECRET_KEY_RE = re.compile(r"(?i)(api[_-]?key|token|secret|password|authorization|credential)")
|
|
44
|
-
_SECRET_VALUE_RE = re.compile(r"\b(?:sk|tp)-[A-Za-z0-9_-]{6,}\b")
|
|
45
|
-
_URL_SCHEMES_TO_SCRUB = frozenset({"http", "https", "ws", "wss"})
|
|
46
|
-
REDACTED = "<redacted>"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _redact_url(value: str) -> str:
|
|
50
|
-
"""Strip URL credentials, query, and fragment while preserving useful host/path."""
|
|
51
|
-
try:
|
|
52
|
-
parts = urlsplit(value)
|
|
53
|
-
except ValueError:
|
|
54
|
-
return value
|
|
55
|
-
if parts.scheme.lower() not in _URL_SCHEMES_TO_SCRUB or not parts.netloc:
|
|
56
|
-
return value
|
|
57
|
-
host = parts.hostname or ""
|
|
58
|
-
if not host:
|
|
59
|
-
return value
|
|
60
|
-
if ":" in host and not host.startswith("["):
|
|
61
|
-
host = f"[{host}]"
|
|
62
|
-
netloc = host
|
|
63
|
-
try:
|
|
64
|
-
port = parts.port
|
|
65
|
-
except ValueError:
|
|
66
|
-
port = None
|
|
67
|
-
if port is not None:
|
|
68
|
-
netloc = f"{netloc}:{port}"
|
|
69
|
-
return urlunsplit((parts.scheme, netloc, parts.path, "", ""))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def redact_secrets(value: Any) -> Any:
|
|
73
|
-
"""Return a JSON-safe copy with likely credentials removed.
|
|
74
|
-
|
|
75
|
-
Centralized for all video-recap QC reports: redacts secret-looking keys,
|
|
76
|
-
common synthetic key/token values, and URL userinfo/query/fragment fields.
|
|
77
|
-
"""
|
|
78
|
-
if isinstance(value, Mapping):
|
|
79
|
-
redacted: dict[str, Any] = {}
|
|
80
|
-
for key, item in value.items():
|
|
81
|
-
key_s = str(key)
|
|
82
|
-
redacted[key_s] = REDACTED if _SECRET_KEY_RE.search(key_s) else redact_secrets(item)
|
|
83
|
-
return redacted
|
|
84
|
-
if isinstance(value, list):
|
|
85
|
-
return [redact_secrets(item) for item in value]
|
|
86
|
-
if isinstance(value, tuple):
|
|
87
|
-
return [redact_secrets(item) for item in value]
|
|
88
|
-
if isinstance(value, Path):
|
|
89
|
-
return str(value)
|
|
90
|
-
if isinstance(value, str):
|
|
91
|
-
return _SECRET_VALUE_RE.sub(REDACTED, _redact_url(value))
|
|
92
|
-
return value
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
_REQUIRED_FINDING_FIELDS = frozenset({
|
|
96
|
-
"finding_id",
|
|
97
|
-
"stage",
|
|
98
|
-
"severity",
|
|
99
|
-
"blocking",
|
|
100
|
-
"deterministic",
|
|
101
|
-
"confidence",
|
|
102
|
-
"rule_id",
|
|
103
|
-
"decision_reason",
|
|
104
|
-
"location",
|
|
105
|
-
"evidence",
|
|
106
|
-
"sample_policy",
|
|
107
|
-
"model_used",
|
|
108
|
-
"next_action",
|
|
109
|
-
# Helper fields kept for current rule semantics and compatibility.
|
|
110
|
-
"category",
|
|
111
|
-
"code",
|
|
112
|
-
"message",
|
|
113
|
-
"source",
|
|
114
|
-
"objective_corroboration",
|
|
115
|
-
})
|
|
116
|
-
_REQUIRED_LOCATION_FIELDS = frozenset({"timecode", "source_span"})
|
|
117
|
-
_REQUIRED_REPORT_FIELDS = frozenset({
|
|
118
|
-
"schema_version",
|
|
119
|
-
"artifact",
|
|
120
|
-
"stage",
|
|
121
|
-
"ok",
|
|
122
|
-
"blocker_count",
|
|
123
|
-
"finding_count",
|
|
124
|
-
"findings",
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
class QCContractError(ValueError):
|
|
129
|
-
"""Raised when a QC report or finding violates the shared contract."""
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def build_finding(
|
|
133
|
-
*,
|
|
134
|
-
stage: str,
|
|
135
|
-
severity: str,
|
|
136
|
-
confidence: str,
|
|
137
|
-
sample_policy: str | Mapping[str, Any],
|
|
138
|
-
category: str,
|
|
139
|
-
code: str,
|
|
140
|
-
message: str,
|
|
141
|
-
finding_id: str | None = None,
|
|
142
|
-
id: str | None = None,
|
|
143
|
-
rule_id: str | None = None,
|
|
144
|
-
decision_reason: str | None = None,
|
|
145
|
-
model_used: str | None = None,
|
|
146
|
-
next_action: str | None = None,
|
|
147
|
-
deterministic: bool | None = None,
|
|
148
|
-
blocking: bool | None = None,
|
|
149
|
-
source: Mapping[str, Any] | None = None,
|
|
150
|
-
location: Mapping[str, Any] | None = None,
|
|
151
|
-
evidence: Mapping[str, Any] | None = None,
|
|
152
|
-
objective_corroboration: Mapping[str, Any] | None = None,
|
|
153
|
-
**extra: Any,
|
|
154
|
-
) -> dict[str, Any]:
|
|
155
|
-
"""Build and validate one normalized QC finding.
|
|
156
|
-
|
|
157
|
-
Deterministic objective findings may block. MiMo semantic/aesthetic findings
|
|
158
|
-
default to advisory and may never block. Objective checks belong in the
|
|
159
|
-
deterministic QC producers instead of upgrading subjective model findings.
|
|
160
|
-
"""
|
|
161
|
-
if finding_id is None:
|
|
162
|
-
finding_id = id
|
|
163
|
-
if finding_id is None:
|
|
164
|
-
raise QCContractError("finding_id is required")
|
|
165
|
-
if rule_id is None:
|
|
166
|
-
rule_id = code
|
|
167
|
-
if decision_reason is None:
|
|
168
|
-
decision_reason = message
|
|
169
|
-
if model_used is None:
|
|
170
|
-
model_used = "local_deterministic"
|
|
171
|
-
if next_action is None:
|
|
172
|
-
next_action = "manual_review"
|
|
173
|
-
if isinstance(sample_policy, str):
|
|
174
|
-
sample_policy = {"type": sample_policy}
|
|
175
|
-
|
|
176
|
-
if deterministic is None:
|
|
177
|
-
deterministic = category in DETERMINISTIC_CATEGORIES
|
|
178
|
-
if source is None:
|
|
179
|
-
source = {}
|
|
180
|
-
source = redact_secrets(source)
|
|
181
|
-
if location is None:
|
|
182
|
-
location = {"timecode": None, "source_span": None}
|
|
183
|
-
else:
|
|
184
|
-
location = {"timecode": location.get("timecode"), "source_span": location.get("source_span")}
|
|
185
|
-
if evidence is None:
|
|
186
|
-
evidence = {}
|
|
187
|
-
evidence = redact_secrets(evidence)
|
|
188
|
-
objective_corroboration = redact_secrets(objective_corroboration or {})
|
|
189
|
-
|
|
190
|
-
is_non_deterministic = (not deterministic) or category in NON_DETERMINISTIC_CATEGORIES
|
|
191
|
-
if is_non_deterministic and blocking is None:
|
|
192
|
-
blocking = False
|
|
193
|
-
elif blocking is None:
|
|
194
|
-
blocking = severity in BLOCKING_SEVERITIES
|
|
195
|
-
|
|
196
|
-
if is_non_deterministic and blocking:
|
|
197
|
-
raise QCContractError(f"non-deterministic blocking finding is not allowed: {category}/{code}")
|
|
198
|
-
|
|
199
|
-
finding = {
|
|
200
|
-
"finding_id": finding_id,
|
|
201
|
-
"id": finding_id,
|
|
202
|
-
"stage": stage,
|
|
203
|
-
"severity": severity,
|
|
204
|
-
"blocking": bool(blocking),
|
|
205
|
-
"deterministic": bool(deterministic),
|
|
206
|
-
"confidence": confidence,
|
|
207
|
-
"rule_id": rule_id,
|
|
208
|
-
"decision_reason": decision_reason,
|
|
209
|
-
"location": dict(location),
|
|
210
|
-
"evidence": dict(evidence),
|
|
211
|
-
"sample_policy": dict(sample_policy),
|
|
212
|
-
"model_used": model_used,
|
|
213
|
-
"next_action": next_action,
|
|
214
|
-
"category": category,
|
|
215
|
-
"code": code,
|
|
216
|
-
"message": message,
|
|
217
|
-
"source": dict(source),
|
|
218
|
-
"objective_corroboration": dict(objective_corroboration),
|
|
219
|
-
}
|
|
220
|
-
finding.update(redact_secrets(extra))
|
|
221
|
-
_validate_finding(finding)
|
|
222
|
-
return finding
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def build_report(
|
|
226
|
-
*,
|
|
227
|
-
artifact: str,
|
|
228
|
-
stage: str,
|
|
229
|
-
findings: Sequence[Mapping[str, Any]] | None = None,
|
|
230
|
-
metadata: Mapping[str, Any] | None = None,
|
|
231
|
-
) -> dict[str, Any]:
|
|
232
|
-
"""Build and validate a minimal report for a supported QC artifact."""
|
|
233
|
-
normalized_findings = [redact_secrets(dict(f)) for f in (findings or [])]
|
|
234
|
-
blocker_count = sum(1 for f in normalized_findings if f.get("blocking") is True)
|
|
235
|
-
report = {
|
|
236
|
-
"schema_version": SCHEMA_VERSION,
|
|
237
|
-
"artifact": artifact,
|
|
238
|
-
"stage": stage,
|
|
239
|
-
"ok": blocker_count == 0,
|
|
240
|
-
"blocker_count": blocker_count,
|
|
241
|
-
"finding_count": len(normalized_findings),
|
|
242
|
-
"findings": normalized_findings,
|
|
243
|
-
"metadata": redact_secrets(dict(metadata or {})),
|
|
244
|
-
}
|
|
245
|
-
validate_report(report)
|
|
246
|
-
return report
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def validate_report(report: Mapping[str, Any]) -> bool:
|
|
250
|
-
"""Validate report shape, value domains, and blocking semantics."""
|
|
251
|
-
if not isinstance(report, Mapping):
|
|
252
|
-
raise QCContractError("report must be an object")
|
|
253
|
-
missing = _REQUIRED_REPORT_FIELDS - set(report)
|
|
254
|
-
if missing:
|
|
255
|
-
raise QCContractError(f"report missing required fields: {sorted(missing)}")
|
|
256
|
-
if report["schema_version"] != SCHEMA_VERSION:
|
|
257
|
-
raise QCContractError(f"unsupported schema_version: {report['schema_version']!r}")
|
|
258
|
-
if report["artifact"] not in ARTIFACTS:
|
|
259
|
-
raise QCContractError(f"unsupported artifact: {report['artifact']!r}")
|
|
260
|
-
if report["stage"] not in STAGES:
|
|
261
|
-
raise QCContractError(f"unsupported stage: {report['stage']!r}")
|
|
262
|
-
if not isinstance(report["findings"], list):
|
|
263
|
-
raise QCContractError("findings must be a list")
|
|
264
|
-
for finding in report["findings"]:
|
|
265
|
-
_validate_finding(finding)
|
|
266
|
-
blocker_count = sum(1 for f in report["findings"] if f.get("blocking") is True)
|
|
267
|
-
if report["blocker_count"] != blocker_count:
|
|
268
|
-
raise QCContractError("blocker_count does not match findings")
|
|
269
|
-
if report["finding_count"] != len(report["findings"]):
|
|
270
|
-
raise QCContractError("finding_count does not match findings")
|
|
271
|
-
if report["ok"] != (blocker_count == 0):
|
|
272
|
-
raise QCContractError("ok must be true exactly when blocker_count is zero")
|
|
273
|
-
return True
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
def _validate_finding(finding: Mapping[str, Any]) -> None:
|
|
277
|
-
if not isinstance(finding, Mapping):
|
|
278
|
-
raise QCContractError("finding must be an object")
|
|
279
|
-
missing = _REQUIRED_FINDING_FIELDS - set(finding)
|
|
280
|
-
if missing:
|
|
281
|
-
raise QCContractError(f"finding missing required fields: {sorted(missing)}")
|
|
282
|
-
if finding["stage"] not in STAGES:
|
|
283
|
-
raise QCContractError(f"unsupported finding stage: {finding['stage']!r}")
|
|
284
|
-
if finding["severity"] not in SEVERITIES:
|
|
285
|
-
raise QCContractError(f"unsupported severity: {finding['severity']!r}")
|
|
286
|
-
if finding["confidence"] not in CONFIDENCES:
|
|
287
|
-
raise QCContractError(f"unsupported confidence: {finding['confidence']!r}")
|
|
288
|
-
sample_policy = finding["sample_policy"]
|
|
289
|
-
if not isinstance(sample_policy, Mapping):
|
|
290
|
-
raise QCContractError("sample_policy must be an object")
|
|
291
|
-
sample_policy_type = sample_policy.get("type")
|
|
292
|
-
if sample_policy_type not in SAMPLE_POLICIES:
|
|
293
|
-
raise QCContractError(f"unsupported sample_policy.type: {sample_policy_type!r}")
|
|
294
|
-
if not isinstance(finding["deterministic"], bool):
|
|
295
|
-
raise QCContractError("deterministic must be boolean")
|
|
296
|
-
if not isinstance(finding["blocking"], bool):
|
|
297
|
-
raise QCContractError("blocking must be boolean")
|
|
298
|
-
if finding["blocking"] and finding["severity"] != "blocker":
|
|
299
|
-
raise QCContractError("blocking findings must use severity='blocker'")
|
|
300
|
-
location = finding["location"]
|
|
301
|
-
if not isinstance(location, Mapping):
|
|
302
|
-
raise QCContractError("location must be an object")
|
|
303
|
-
missing_location = _REQUIRED_LOCATION_FIELDS - set(location)
|
|
304
|
-
if missing_location:
|
|
305
|
-
raise QCContractError(f"location missing required fields: {sorted(missing_location)}")
|
|
306
|
-
if finding["source"] is None or not isinstance(finding["source"], Mapping):
|
|
307
|
-
raise QCContractError("source must be an object")
|
|
308
|
-
if finding["evidence"] is None or not isinstance(finding["evidence"], Mapping):
|
|
309
|
-
raise QCContractError("evidence must be an object")
|
|
310
|
-
if finding["objective_corroboration"] is None or not isinstance(finding["objective_corroboration"], Mapping):
|
|
311
|
-
raise QCContractError("objective_corroboration must be an object")
|
|
312
|
-
for field in ("finding_id", "rule_id", "decision_reason", "model_used", "next_action"):
|
|
313
|
-
if not isinstance(finding[field], str) or not finding[field]:
|
|
314
|
-
raise QCContractError(f"{field} must be a non-empty string")
|
|
315
|
-
|
|
316
|
-
is_non_deterministic = (not finding["deterministic"]) or finding["category"] in NON_DETERMINISTIC_CATEGORIES
|
|
317
|
-
if is_non_deterministic and finding["blocking"]:
|
|
318
|
-
raise QCContractError(
|
|
319
|
-
f"non-deterministic blocking finding is not allowed: {finding['category']}/{finding['code']}"
|
|
320
|
-
)
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""Fuse scenes, ASR, and quiet windows for narration planning."""
|
|
2
|
-
|
|
3
|
-
from lib import CONFIG
|
|
4
|
-
from agent_text import _overlap_seconds, _recommended_char_budget
|
|
5
|
-
from narration_lint import _validate_narration_budget
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def _quiet_windows(silence_periods):
|
|
9
|
-
return [qp for qp in silence_periods if not qp["has_speech"]]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def _align_narration_to_quiet(narration, scenes_analysis, silence_periods):
|
|
13
|
-
"""Recompute overlaps_speech from real quiet windows; keep the agent's timing.
|
|
14
|
-
|
|
15
|
-
The dense continuous-bed design places narration ON the pictured beat over a
|
|
16
|
-
ducked original bed, so segments are never relocated into silence gaps. Only
|
|
17
|
-
the overlaps_speech flag that the ducking stage consumes is corrected, leaving
|
|
18
|
-
the agent's start/end (and text) intact.
|
|
19
|
-
|
|
20
|
-
Budget/dedup runs FIRST so a dedup-merged beat's overlaps_speech reflects its
|
|
21
|
-
extended timing, not its original shorter span.
|
|
22
|
-
"""
|
|
23
|
-
aligned = _validate_narration_budget(narration, scenes_analysis)
|
|
24
|
-
quiet_windows = _quiet_windows(silence_periods)
|
|
25
|
-
quiet_ratio_min = CONFIG["quiet_overlap_min_ratio"]
|
|
26
|
-
for n in aligned:
|
|
27
|
-
seg_dur = n["end"] - n["start"]
|
|
28
|
-
quiet_overlap = sum(
|
|
29
|
-
_overlap_seconds(n["start"], n["end"], qw["start"], qw["end"])
|
|
30
|
-
for qw in quiet_windows
|
|
31
|
-
)
|
|
32
|
-
n["overlaps_speech"] = quiet_overlap < max(0.3, seg_dur * quiet_ratio_min)
|
|
33
|
-
return aligned
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _scene_asr_lines(asr_result, scene):
|
|
37
|
-
lines = []
|
|
38
|
-
for seg in asr_result:
|
|
39
|
-
if scene["start"] < seg["end"] and scene["end"] > seg["start"] and seg["text"]:
|
|
40
|
-
lines.append(f" [{seg['start']:.1f}-{seg['end']:.1f}] {seg['text']}")
|
|
41
|
-
return lines
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _quiet_windows_for_scene(silence_periods, scene):
|
|
45
|
-
windows = []
|
|
46
|
-
for qp in _quiet_windows(silence_periods):
|
|
47
|
-
if qp["start"] < scene["end"] and qp["end"] > scene["start"]:
|
|
48
|
-
start = max(qp["start"], scene["start"])
|
|
49
|
-
end = min(qp["end"], scene["end"])
|
|
50
|
-
if end > start:
|
|
51
|
-
windows.append((start, end))
|
|
52
|
-
return windows
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _build_timeline_fusion(scenes, asr_segments, silence_periods):
|
|
56
|
-
"""Fuse VLM scenes, ASR dialogue and quiet narration slots on one timeline."""
|
|
57
|
-
fusion = []
|
|
58
|
-
quiet_windows = _quiet_windows(silence_periods)
|
|
59
|
-
for scene in scenes:
|
|
60
|
-
start, end = scene["start"], scene["end"]
|
|
61
|
-
dialogue_segments = []
|
|
62
|
-
dialogue_overlap = 0.0
|
|
63
|
-
for seg in asr_segments:
|
|
64
|
-
overlap = _overlap_seconds(start, end, seg["start"], seg["end"])
|
|
65
|
-
if overlap <= 0:
|
|
66
|
-
continue
|
|
67
|
-
dialogue_overlap += overlap
|
|
68
|
-
dialogue_segments.append(
|
|
69
|
-
{
|
|
70
|
-
"start": round(seg["start"], 2),
|
|
71
|
-
"end": round(seg["end"], 2),
|
|
72
|
-
"overlap_seconds": round(overlap, 2),
|
|
73
|
-
"text": seg["text"],
|
|
74
|
-
}
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
narration_slots = []
|
|
78
|
-
for window in quiet_windows:
|
|
79
|
-
overlap = _overlap_seconds(start, end, window["start"], window["end"])
|
|
80
|
-
if overlap <= 0:
|
|
81
|
-
continue
|
|
82
|
-
slot_start = max(start, window["start"])
|
|
83
|
-
slot_end = min(end, window["end"])
|
|
84
|
-
narration_slots.append(
|
|
85
|
-
{
|
|
86
|
-
"start": round(slot_start, 2),
|
|
87
|
-
"end": round(slot_end, 2),
|
|
88
|
-
"duration": round(slot_end - slot_start, 2),
|
|
89
|
-
"char_budget": _recommended_char_budget(slot_start, slot_end),
|
|
90
|
-
}
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
fusion.append(
|
|
94
|
-
{
|
|
95
|
-
"scene_id": scene["scene_id"],
|
|
96
|
-
"time_range": [round(start, 2), round(end, 2)],
|
|
97
|
-
"visual_description": scene["description"],
|
|
98
|
-
"depth_analysis": scene.get("depth_analysis", ""),
|
|
99
|
-
"frame_facts": scene.get("frame_facts", {}),
|
|
100
|
-
"dialogue_segments": dialogue_segments,
|
|
101
|
-
"dialogue_overlap_seconds": round(dialogue_overlap, 2),
|
|
102
|
-
"narration_slots": narration_slots,
|
|
103
|
-
"recommended_mode": "quiet-slot"
|
|
104
|
-
if narration_slots and dialogue_overlap < (end - start) * 0.4
|
|
105
|
-
else "ducked-bed",
|
|
106
|
-
}
|
|
107
|
-
)
|
|
108
|
-
return fusion
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"""Deterministic narration readability/QC scanner.
|
|
2
|
-
|
|
3
|
-
This module is intentionally report-only. It flags local readability and
|
|
4
|
-
packaging hygiene risks for video narration; it is not an AIGC detector and it
|
|
5
|
-
never rewrites text.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
import re
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
from typing import Any
|
|
14
|
-
|
|
15
|
-
CONTRACT = (
|
|
16
|
-
"Local readability/QC report only: this is not an AIGC detector, does not "
|
|
17
|
-
"claim AI-generation accuracy, and never rewrites text. Corrections remain "
|
|
18
|
-
"human/agent rewrite work."
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
EM_DASH_RE = re.compile(r"——|—")
|
|
22
|
-
NEGATIVE_FLIP_RE = re.compile(r"并?不是[^。!?!?;;\.]{0,40}而是")
|
|
23
|
-
# Catch verbatim copy of the brief's example scaffolding without over-matching real text.
|
|
24
|
-
# The bracketed role token 【主角】 is the placeholder; anchor the pronoun signal to the
|
|
25
|
-
# example's exact phrase "TA要赌上" so the legitimate gender-neutral pronoun "TA" (他/她) +
|
|
26
|
-
# 要 (idiomatic 解说 suspense device "凶手就在其中,TA要做的下一件事…") is NOT hard-blocked.
|
|
27
|
-
PLACEHOLDER_RE = re.compile(r"【主角】|TA要赌上")
|
|
28
|
-
|
|
29
|
-
CLICHE_TERMS = [
|
|
30
|
-
"命运的齿轮", "殊不知", "与此同时", "一场阴谋", "背后真相", "不为人知",
|
|
31
|
-
"故事就此展开", "命运", "真相", "秘密", "危机", "反转", "救赎",
|
|
32
|
-
]
|
|
33
|
-
ABSTRACT_TERMS = ["人性", "成长", "救赎", "命运", "时代", "宿命", "选择", "意义", "价值", "情感"]
|
|
34
|
-
REASONING_MARKERS = ["因为", "所以", "因此", "也就是说", "换句话说", "这意味着", "原因是", "可见"]
|
|
35
|
-
METAPHOR_MARKERS = ["像", "仿佛", "好似", "犹如", "如同", "一把", "一场", "一张", "棋局", "风暴"]
|
|
36
|
-
CONNECTIVE_MARKERS = ["但", "却", "而", "于是", "随后", "直到", "结果", "因为", "所以", "这时", "最后"]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _sentence_pieces(text: str) -> list[str]:
|
|
40
|
-
"""Split text into sentence-like pieces while keeping terminal punctuation."""
|
|
41
|
-
text = re.sub(r"\s+", " ", text).strip()
|
|
42
|
-
if not text:
|
|
43
|
-
return []
|
|
44
|
-
parts = re.split(r"([。!?!?;;.])", text)
|
|
45
|
-
out: list[str] = []
|
|
46
|
-
for idx in range(0, len(parts), 2):
|
|
47
|
-
body = parts[idx].strip()
|
|
48
|
-
punct = parts[idx + 1] if idx + 1 < len(parts) else ""
|
|
49
|
-
if body or punct:
|
|
50
|
-
out.append((body + punct).strip())
|
|
51
|
-
return out or [text]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _text_units(text: str) -> int:
|
|
55
|
-
"""Length unit for prose: CJK characters, otherwise words."""
|
|
56
|
-
if re.search(r"[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]", text):
|
|
57
|
-
return len(re.sub(r"\s+", "", text))
|
|
58
|
-
return len(re.findall(r"\b\w+\b", text))
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _normalise_segments(payload: list[dict[str, Any]], *, source: str) -> list[dict[str, Any]]:
|
|
62
|
-
key = "narration" if source == "narration" else "text"
|
|
63
|
-
segments: list[dict[str, Any]] = []
|
|
64
|
-
for idx, item in enumerate(payload):
|
|
65
|
-
text = str(item.get(key, "")).strip()
|
|
66
|
-
if text:
|
|
67
|
-
segments.append({"source": source, "index": idx, "text": text})
|
|
68
|
-
return segments
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _load_original_subtitles(work_dir: Path | None) -> list[dict[str, Any]]:
|
|
72
|
-
if work_dir is None:
|
|
73
|
-
return []
|
|
74
|
-
path = work_dir / "original_subtitles.json"
|
|
75
|
-
if not path.exists():
|
|
76
|
-
return []
|
|
77
|
-
return _normalise_segments(json.loads(path.read_text(encoding="utf-8")), source="original_subtitles")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _style_card_requirement(work_dir: Path | None) -> tuple[bool, str]:
|
|
81
|
-
"""Read the explicit requirements contract; workspaces without one are advisory-only."""
|
|
82
|
-
if work_dir is None:
|
|
83
|
-
return False, "legacy_default"
|
|
84
|
-
path = work_dir / "deslop_qc_requirements.json"
|
|
85
|
-
if not path.exists():
|
|
86
|
-
return False, "legacy_default"
|
|
87
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
88
|
-
return data["style_card_required"], "deslop_qc_requirements.json"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _style_card_issue(work_dir: Path | None, required: bool) -> dict[str, Any] | None:
|
|
92
|
-
if work_dir is None:
|
|
93
|
-
return None
|
|
94
|
-
severity = "blocker" if required else "advisory"
|
|
95
|
-
path = work_dir / "style_card.json"
|
|
96
|
-
if not path.exists():
|
|
97
|
-
return {
|
|
98
|
-
"severity": severity,
|
|
99
|
-
"code": "missing_style_card",
|
|
100
|
-
"source": "style_card",
|
|
101
|
-
"index": None,
|
|
102
|
-
"message": (
|
|
103
|
-
"style_card.json is required by this expression/packaging run but is missing"
|
|
104
|
-
if required else
|
|
105
|
-
"style_card.json is absent; legacy/migration workspaces may continue, but new expression-special runs should author it"
|
|
106
|
-
),
|
|
107
|
-
}
|
|
108
|
-
data = json.loads(path.read_text(encoding="utf-8"))
|
|
109
|
-
if not isinstance(data, dict) or not data:
|
|
110
|
-
return {
|
|
111
|
-
"severity": severity,
|
|
112
|
-
"code": "malformed_style_card",
|
|
113
|
-
"source": "style_card",
|
|
114
|
-
"index": None,
|
|
115
|
-
"message": (
|
|
116
|
-
"style_card.json is required but empty or not a JSON object"
|
|
117
|
-
if required else
|
|
118
|
-
"style_card.json is present but empty or not a JSON object; treat as migration warning unless the run requires it"
|
|
119
|
-
),
|
|
120
|
-
}
|
|
121
|
-
return None
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _add_issue(bucket: list[dict[str, Any]], severity: str, code: str, source: str, index: int | None, message: str, **extra: Any) -> None:
|
|
125
|
-
issue = {"severity": severity, "code": code, "source": source, "index": index, "message": message}
|
|
126
|
-
issue.update({k: v for k, v in extra.items() if v is not None})
|
|
127
|
-
bucket.append(issue)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def analyze_deslop_qc(narration: list[dict[str, Any]], *, work_dir: str | Path | None = None, original_subtitles: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
|
131
|
-
"""Return a structured report; never mutates or rewrites input text."""
|
|
132
|
-
work_path = Path(work_dir) if work_dir is not None else None
|
|
133
|
-
segments = _normalise_segments(narration, source="narration")
|
|
134
|
-
if original_subtitles is None:
|
|
135
|
-
segments.extend(_load_original_subtitles(work_path))
|
|
136
|
-
else:
|
|
137
|
-
segments.extend(_normalise_segments(original_subtitles, source="original_subtitles"))
|
|
138
|
-
|
|
139
|
-
blockers: list[dict[str, Any]] = []
|
|
140
|
-
advisories: list[dict[str, Any]] = []
|
|
141
|
-
|
|
142
|
-
required, requirement_source = _style_card_requirement(work_path)
|
|
143
|
-
style_issue = _style_card_issue(work_path, required)
|
|
144
|
-
if style_issue:
|
|
145
|
-
(blockers if style_issue["severity"] == "blocker" else advisories).append(style_issue)
|
|
146
|
-
|
|
147
|
-
all_text = "\n".join(seg["text"] for seg in segments)
|
|
148
|
-
total_units = max(1, _text_units(all_text))
|
|
149
|
-
sentences: list[str] = []
|
|
150
|
-
for seg in segments:
|
|
151
|
-
text = seg["text"]
|
|
152
|
-
if EM_DASH_RE.search(text):
|
|
153
|
-
_add_issue(blockers, "blocker", "em_dash", seg["source"], seg["index"], "破折号(—/——)不得出现在 narration/original_subtitles 中", matches=EM_DASH_RE.findall(text))
|
|
154
|
-
if PLACEHOLDER_RE.search(text):
|
|
155
|
-
_add_issue(blockers, "blocker", "placeholder_leakage", seg["source"], seg["index"], "示例占位内容泄漏到成稿中(如【主角】/TA要赌上)", matches=PLACEHOLDER_RE.findall(text))
|
|
156
|
-
for sentence in _sentence_pieces(text):
|
|
157
|
-
sentences.append(sentence)
|
|
158
|
-
if NEGATIVE_FLIP_RE.search(sentence):
|
|
159
|
-
_add_issue(advisories, "advisory", "negative_positive_flip", seg["source"], seg["index"], "“不是…而是…”模板化转折偏多,建议改成更具体的因果/行动表达", sentence=sentence[:120])
|
|
160
|
-
|
|
161
|
-
def term_count(terms: list[str]) -> int:
|
|
162
|
-
return sum(all_text.count(term) for term in terms)
|
|
163
|
-
|
|
164
|
-
cliche_count = term_count(CLICHE_TERMS)
|
|
165
|
-
abstract_count = term_count(ABSTRACT_TERMS)
|
|
166
|
-
reasoning_count = term_count(REASONING_MARKERS)
|
|
167
|
-
metaphor_count = term_count(METAPHOR_MARKERS)
|
|
168
|
-
connective_count = term_count(CONNECTIVE_MARKERS)
|
|
169
|
-
|
|
170
|
-
if cliche_count >= 4 or (cliche_count / total_units) > 0.025:
|
|
171
|
-
_add_issue(advisories, "advisory", "cliche_density", "narration", None, "套话/高频抽象词偏密,建议换成具体行动、选择和后果", count=cliche_count)
|
|
172
|
-
if abstract_count >= 5 or (abstract_count / total_units) > 0.03:
|
|
173
|
-
_add_issue(advisories, "advisory", "abstract_summary", "narration", None, "抽象总结词偏多,容易像概括论文;补足画面证据和具体抉择", count=abstract_count)
|
|
174
|
-
if reasoning_count >= 4:
|
|
175
|
-
_add_issue(advisories, "advisory", "reasoning_chain", "narration", None, "解释链标记偏多,建议减少“因为/所以/这意味着”等讲理口吻", count=reasoning_count)
|
|
176
|
-
if metaphor_count >= 4:
|
|
177
|
-
_add_issue(advisories, "advisory", "metaphor_markers", "narration", None, "比喻/包装化标记偏多,确认没有遮住剧情事实", count=metaphor_count)
|
|
178
|
-
if total_units >= 180 and connective_count <= 1:
|
|
179
|
-
_add_issue(advisories, "advisory", "low_connective_density", "narration", None, "长文本缺少转折/因果连接,可能像平铺摘要", connective_count=connective_count)
|
|
180
|
-
|
|
181
|
-
long_segments = [seg for seg in segments if seg["source"] == "narration" and _text_units(seg["text"]) > 180]
|
|
182
|
-
if long_segments:
|
|
183
|
-
_add_issue(advisories, "advisory", "overlong_narration_block", "narration", long_segments[0]["index"], "单个解说块过长,建议拆成更可听的段落", max_units=max(_text_units(seg["text"]) for seg in long_segments))
|
|
184
|
-
long_sentences = [s for s in sentences if _text_units(s) > 90]
|
|
185
|
-
if long_sentences:
|
|
186
|
-
_add_issue(advisories, "advisory", "long_paragraph", "narration", None, "长句/长段落偏多,字幕阅读压力较大", max_units=max(_text_units(s) for s in long_sentences))
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
"ok": not blockers,
|
|
190
|
-
"contract": CONTRACT,
|
|
191
|
-
"scanner": "deslop_qc.py",
|
|
192
|
-
"style_card_required": required,
|
|
193
|
-
"style_card_requirement_source": requirement_source,
|
|
194
|
-
"blocker_count": len(blockers),
|
|
195
|
-
"advisory_count": len(advisories),
|
|
196
|
-
"blockers": blockers,
|
|
197
|
-
"advisories": advisories,
|
|
198
|
-
"metrics": {
|
|
199
|
-
"segments_scanned": len(segments),
|
|
200
|
-
"text_units": total_units if all_text else 0,
|
|
201
|
-
"sentence_count": len(sentences),
|
|
202
|
-
"cliche_count": cliche_count,
|
|
203
|
-
"abstract_count": abstract_count,
|
|
204
|
-
"reasoning_marker_count": reasoning_count,
|
|
205
|
-
"metaphor_marker_count": metaphor_count,
|
|
206
|
-
"connective_count": connective_count,
|
|
207
|
-
},
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
__all__ = ["CONTRACT", "analyze_deslop_qc"]
|