@oh-story/dsh 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -11
- package/lib/client.js +85 -35
- package/lib/client.js.map +3 -3
- package/lib/drama/manifest.json +41 -36
- package/lib/drama/skills/short-drama/scripts/creator_markdown_check.py +340 -13
- package/lib/drama/skills/short-drama-assets/references/voice-direction.md +4 -2
- package/lib/drama/skills/short-drama-edit/SKILL.md +32 -12
- package/lib/drama/skills/short-drama-edit/references/still-cuts.md +90 -0
- package/lib/drama/skills/short-drama-edit/scripts/edit_tool.py +1035 -145
- package/lib/drama/skills/short-drama-edit/scripts/selftest.py +48 -0
- package/lib/drama/skills/short-drama-produce/SKILL.md +6 -1
- package/lib/drama/skills/short-drama-produce/references/adapter-contract.md +12 -0
- package/lib/drama/skills/short-drama-produce/scripts/production_tool.py +138 -9
- package/lib/drama/skills/short-drama-produce/scripts/provider_adapters.py +6 -0
- package/lib/drama/skills/short-drama-storyboard/SKILL.md +2 -0
- package/lib/drama/skills/short-drama-storyboard/references/production-shot-grammar.md +3 -1
- package/lib/drama/skills/short-drama-video-prompts/SKILL.md +7 -0
- package/lib/drama/skills/short-drama-video-prompts/references/performance-action-timing.md +14 -0
- package/lib/drama/skills/short-drama-video-prompts/references/seedance-2.5.md +19 -6
- package/lib/drama/skills/short-drama-video-prompts/references/stage-contract.md +1 -0
- package/lib/drama/skills/short-drama-video-prompts/references/target-model-profile.md +5 -0
- package/lib/drama/skills/short-drama-video-prompts/references/wan-3.0.md +4 -1
- package/lib/index.js +16 -7
- package/lib/index.js.map +2 -2
- package/lib/novel-to-game/README.md +98 -44
- package/lib/novel-to-game/README_ZH.md +100 -46
- package/lib/novel-to-game/VERSION +1 -1
- package/lib/novel-to-game/examples/jin-ping-mei/build/BUILD_BRIEF.md +27 -202
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/RUN.md +23 -115
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/chamber.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_night.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/court_rain.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/garden.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/hall.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/kitchen.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/street_dawn.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/bg/temple.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/yue/moon.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/meng/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pan/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/pinger/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/xuee/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/angry.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/blush.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/neutral.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/sad.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/smile.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/sprite/yue/surprised.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/style.css +326 -2503
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/index.html +71 -4
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/adv.js +221 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/audio.js +113 -257
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/main.js +511 -3026
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/menus.js +277 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/petals.js +67 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/stage.js +180 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/store.js +70 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common2.js +204 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/common3.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/dates.js +186 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/index.js +122 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/meng.js +190 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pan.js +212 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/pinger.js +198 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/xuee.js +199 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/story/yue.js +214 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/package.json +2 -2
- package/lib/novel-to-game/examples/jin-ping-mei/build/art/generated-art.json +182 -45
- package/lib/novel-to-game/examples/jin-ping-mei/qa/README.md +7 -17
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verification.json +9 -9
- package/lib/novel-to-game/manifest.json +303 -78
- package/lib/novel-to-game/skills/game-art-direction/references/art-direction-method.md +7 -2
- package/lib/novel-to-game/skills/game-concept/references/concept-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/narrative-design-method.md +2 -0
- package/lib/novel-to-game/skills/game-world-design/references/world-design-method.md +9 -0
- package/lib/types/client/drama-production-view.d.ts +11 -0
- package/lib/types/client/drama-production-view.d.ts.map +1 -1
- package/lib/types/client/file-activity.d.ts +1 -1
- package/lib/types/client/file-activity.d.ts.map +1 -1
- package/lib/types/client/production-prompts.d.ts +7 -1
- package/lib/types/client/production-prompts.d.ts.map +1 -1
- package/lib/types/client/production-runtime.d.ts +7 -2
- package/lib/types/client/production-runtime.d.ts.map +1 -1
- package/lib/types/drama-adapters.d.ts.map +1 -1
- package/lib/types/skill-provider.d.ts.map +1 -1
- package/lib/types/video-project.d.ts.map +1 -1
- package/lib/video-recap/CHANGELOG.md +108 -1
- package/lib/video-recap/README.en.md +18 -14
- package/lib/video-recap/README.md +18 -14
- package/lib/video-recap/manifest.json +297 -321
- package/lib/video-recap/skills/video-assemble/SKILL.md +18 -16
- package/lib/video-recap/skills/video-assemble/references/explicit-audio-mix.md +3 -1
- package/lib/video-recap/skills/video-assemble/references/jianying/SOURCE.md +5 -5
- package/lib/video-recap/skills/video-assemble/references/packaging.md +4 -4
- package/lib/video-recap/skills/video-assemble/references/subtitle-track.md +10 -10
- package/lib/video-recap/skills/video-assemble/scripts/adoption/audio_mix_binding.py +13 -2
- package/lib/video-recap/skills/video-assemble/scripts/adoption/av_clock.py +109 -0
- package/lib/video-recap/skills/video-assemble/scripts/adoption/strict_publish.py +4 -1
- package/lib/video-recap/skills/video-assemble/scripts/artifacts.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/assemble.py +188 -51
- package/lib/video-recap/skills/video-assemble/scripts/assemble_constants.py +0 -1
- package/lib/video-recap/skills/video-assemble/scripts/assembly_contract.py +32 -75
- package/lib/video-recap/skills/video-assemble/scripts/assembly_settings.py +8 -15
- package/lib/video-recap/skills/video-assemble/scripts/audio_mix.py +202 -161
- package/lib/video-recap/skills/video-assemble/scripts/codec_peak.py +112 -0
- package/lib/video-recap/skills/video-assemble/scripts/export_jianying.py +10 -101
- package/lib/video-recap/skills/video-assemble/scripts/jianying/builders.py +44 -475
- package/lib/video-recap/skills/video-assemble/scripts/jianying/model.py +1 -22
- package/lib/video-recap/skills/video-assemble/scripts/jianying/templates.py +0 -3
- package/lib/video-recap/skills/video-assemble/scripts/jianying/timeline_contract.py +36 -103
- package/lib/video-recap/skills/video-assemble/scripts/jianying/tracks.py +0 -8
- package/lib/video-recap/skills/video-assemble/scripts/jianying/writer.py +28 -203
- package/lib/video-recap/skills/video-assemble/scripts/lib.py +20 -34
- package/lib/video-recap/skills/video-assemble/scripts/loudness.py +332 -0
- package/lib/video-recap/skills/video-assemble/scripts/media.py +158 -38
- package/lib/video-recap/skills/video-assemble/scripts/narration_audio.py +30 -16
- package/lib/video-recap/skills/video-assemble/scripts/render_preflight.py +49 -8
- package/lib/video-recap/skills/video-assemble/scripts/source_subtitles.py +17 -6
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/core.py +24 -2
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track.py +2 -5
- package/lib/video-recap/skills/video-assemble/scripts/subtitles/track_binding.py +2 -9
- package/lib/video-recap/skills/video-assemble/scripts/timeline.py +12 -34
- package/lib/video-recap/skills/video-assemble/scripts/timeline_emit.py +3 -4
- package/lib/video-recap/skills/video-assemble/scripts/visual_render.py +28 -2
- package/lib/video-recap/skills/video-cut/SKILL.md +37 -11
- package/lib/video-recap/skills/video-cut/references/shot-review.md +1 -1
- package/lib/video-recap/skills/video-cut/scripts/cut.py +2 -34
- package/lib/video-recap/skills/video-cut/scripts/cut_cli.py +96 -124
- package/lib/video-recap/skills/video-cut/scripts/cut_contract.py +97 -182
- package/lib/video-recap/skills/video-cut/scripts/cut_render.py +177 -166
- package/lib/video-recap/skills/video-cut/scripts/frame_grid.py +249 -0
- package/lib/video-recap/skills/video-cut/scripts/lib.py +0 -3
- package/lib/video-recap/skills/video-cut/scripts/media_geometry.py +173 -7
- package/lib/video-recap/skills/video-cut/scripts/sentence_boundaries.py +234 -151
- package/lib/video-recap/skills/video-cut/scripts/sentence_gate.py +189 -0
- package/lib/video-recap/skills/video-cut/scripts/shot_review.py +2 -6
- package/lib/video-recap/skills/video-recap/SKILL.md +24 -27
- package/lib/video-recap/skills/video-recap/references/audio-routing.md +8 -6
- package/lib/video-recap/skills/video-recap/references/config-playbook.md +8 -11
- package/lib/video-recap/skills/video-recap/references/data-schema.md +131 -74
- package/lib/video-recap/skills/video-recap/references/resource-library.md +17 -1
- package/lib/video-recap/skills/video-recap/scripts/doctor.py +51 -268
- package/lib/video-recap/skills/video-recap/scripts/final_qc.py +89 -254
- package/lib/video-recap/skills/video-recap/scripts/lib.py +81 -70
- package/lib/video-recap/skills/video-recap/scripts/library.py +40 -4
- package/lib/video-recap/skills/video-recap/scripts/materials.py +13 -80
- package/lib/video-recap/skills/video-recap/scripts/recap_cli.py +49 -17
- package/lib/video-recap/skills/video-recap/scripts/recap_inspect.py +1 -3
- package/lib/video-recap/skills/video-recap/scripts/recap_runner.py +86 -116
- package/lib/video-recap/skills/video-recap/scripts/recap_runtime.py +87 -23
- package/lib/video-recap/skills/video-recap/scripts/recap_source.py +0 -39
- package/lib/video-recap/skills/video-recap/scripts/recap_stage_qc.py +37 -135
- package/lib/video-recap/skills/video-recap/scripts/recap_timeline.py +118 -127
- package/lib/video-recap/skills/video-recap/scripts/resources/__init__.py +4 -0
- package/lib/video-recap/skills/video-recap/scripts/{resource_lock.py → resources/lock.py} +1 -1
- package/lib/video-recap/skills/video-recap/scripts/{project_binding.py → resources/project_binding.py} +78 -17
- package/lib/video-recap/skills/video-reference/SKILL.md +108 -0
- package/lib/video-recap/skills/video-reference/references/reference-schema.md +139 -0
- package/lib/video-recap/skills/video-reference/scripts/lib.py +85 -0
- package/lib/video-recap/skills/video-reference/scripts/reference.py +96 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_check.py +553 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_frames.py +122 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_measure.py +253 -0
- package/lib/video-recap/skills/video-reference/scripts/reference_profile.py +271 -0
- package/lib/video-recap/skills/video-script/SKILL.md +9 -7
- package/lib/video-recap/skills/video-script/scripts/agent_text.py +18 -267
- package/lib/video-recap/skills/video-script/scripts/deslop_qc.py +12 -30
- package/lib/video-recap/skills/video-script/scripts/entry_suggestion.py +125 -0
- package/lib/video-recap/skills/video-script/scripts/evidence_bundle.py +2 -26
- package/lib/video-recap/skills/video-script/scripts/lib.py +5 -15
- package/lib/video-recap/skills/video-script/scripts/lint_summary.py +184 -0
- package/lib/video-recap/skills/video-script/scripts/narration_lint.py +120 -100
- package/lib/video-recap/skills/video-script/scripts/review.py +3 -34
- package/lib/video-recap/skills/video-script/scripts/review_grounding.py +20 -46
- package/lib/video-recap/skills/video-script/scripts/review_response.py +120 -258
- package/lib/video-recap/skills/video-script/scripts/review_runner.py +2 -10
- package/lib/video-recap/skills/video-script/scripts/speech_ownership.py +89 -5
- package/lib/video-recap/skills/video-script/scripts/validate.py +26 -34
- package/lib/video-recap/skills/video-understanding/SKILL.md +29 -4
- package/lib/video-recap/skills/video-understanding/references/data-schema.md +5 -22
- package/lib/video-recap/skills/video-understanding/scripts/agent_text.py +24 -121
- package/lib/video-recap/skills/video-understanding/scripts/asr.py +11 -5
- package/lib/video-recap/skills/video-understanding/scripts/asr_timing_evidence.py +15 -24
- package/lib/video-recap/skills/video-understanding/scripts/briefing/builder.py +59 -25
- package/lib/video-recap/skills/video-understanding/scripts/briefing/context.py +39 -3
- package/lib/video-recap/skills/video-understanding/scripts/briefing/inputs.py +15 -27
- package/lib/video-recap/skills/video-understanding/scripts/briefing/timeline.py +136 -37
- package/lib/video-recap/skills/video-understanding/scripts/consolidate.py +121 -37
- package/lib/video-recap/skills/video-understanding/scripts/detect.py +118 -16
- package/lib/video-recap/skills/video-understanding/scripts/index_normalize.py +359 -0
- package/lib/video-recap/skills/video-understanding/scripts/lib.py +62 -21
- package/lib/video-recap/skills/video-understanding/scripts/storyboard.py +181 -51
- package/lib/video-recap/skills/video-understanding/scripts/timeline_fusion.py +0 -26
- package/lib/video-recap/skills/video-understanding/scripts/understanding_brief.py +37 -14
- package/lib/video-recap/skills/video-understanding/scripts/understanding_cache.py +40 -32
- package/lib/video-recap/skills/video-understanding/scripts/understanding_runner.py +100 -127
- package/lib/video-recap/skills/video-understanding/scripts/understanding_storyboard.py +182 -30
- package/lib/video-recap/skills/video-understanding/scripts/vlm.py +30 -27
- package/lib/video-recap/skills/video-voiceover/SKILL.md +19 -12
- package/lib/video-recap/skills/video-voiceover/references/index-tts.md +3 -3
- package/lib/video-recap/skills/video-voiceover/scripts/approved_text_policy.py +3 -1
- package/lib/video-recap/skills/video-voiceover/scripts/dub.py +60 -135
- package/lib/video-recap/skills/video-voiceover/scripts/lib.py +9 -34
- package/lib/video-recap/skills/video-voiceover/scripts/providers/fish_audio.py +6 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_audio.py +64 -1
- package/lib/video-recap/skills/video-voiceover/scripts/tts_cache.py +99 -0
- package/lib/video-recap/skills/video-voiceover/scripts/voiceover.py +177 -200
- package/package.json +45 -45
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/css/new-guofeng.css +0 -1494
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/assets.js +0 -147
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/data.js +0 -7470
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/engine.js +0 -13066
- package/lib/novel-to-game/examples/jin-ping-mei/build/app/js/text.js +0 -32
- package/lib/novel-to-game/examples/jin-ping-mei/qa/playable-model.json +0 -99
- package/lib/novel-to-game/examples/jin-ping-mei/qa/verify_playable_model.mjs +0 -152
- package/lib/video-recap/skills/video-assemble/references/foreground-compose.md +0 -118
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +0 -84
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +0 -98
- package/lib/video-recap/skills/video-assemble/references/jianying/empty_jy_draft.json +0 -204
- package/lib/video-recap/skills/video-assemble/references/pair-media.md +0 -74
- package/lib/video-recap/skills/video-assemble/scripts/compose_foreground.py +0 -293
- package/lib/video-recap/skills/video-assemble/scripts/pair_media.py +0 -216
- package/lib/video-recap/skills/video-recap/references/shift-left-qc-schema.md +0 -23
- package/lib/video-recap/skills/video-recap/scripts/mimo_qc.py +0 -33
- package/lib/video-recap/skills/video-recap/scripts/qc/__init__.py +0 -1
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_client.py +0 -13
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_contract.py +0 -9
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_evidence.py +0 -287
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_observations.py +0 -175
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_payload.py +0 -206
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_report.py +0 -318
- package/lib/video-recap/skills/video-recap/scripts/qc/mimo_runner.py +0 -130
- package/lib/video-recap/skills/video-recap/scripts/qc_contract.py +0 -320
- package/lib/video-recap/skills/video-script/scripts/timeline_fusion.py +0 -108
- package/lib/video-recap/skills/video-understanding/scripts/brief.py +0 -9
- package/lib/video-recap/skills/video-understanding/scripts/deslop_qc.py +0 -211
- package/lib/video-recap/skills/video-understanding/scripts/narration_lint.py +0 -707
- package/lib/video-recap/skills/video-understanding/scripts/speech_ownership.py +0 -143
- /package/lib/video-recap/skills/video-cut/scripts/{narration_mapping.py → cut_qc.py} +0 -0
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Pair explicit picture and adopted audio assets without re-encoding either stream.
|
|
3
|
-
|
|
4
|
-
This is asset pairing, not a claim of source reconstruction, correct editorial
|
|
5
|
-
selection, speech alignment, or release approval. Existing assemble then consumes
|
|
6
|
-
paired.mp4; its subtitle binding must be newly computed on that container/a:0.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import argparse
|
|
10
|
-
from fractions import Fraction
|
|
11
|
-
import json
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
import subprocess
|
|
14
|
-
|
|
15
|
-
from assemble_constants import SUPPORTED_PICTURE_CODECS
|
|
16
|
-
from adoption.frozen_audio import probe_audio_packets, verify_adopted_audio
|
|
17
|
-
from adoption.strict_inputs import (
|
|
18
|
-
probe_json, require_declared_path, require_fields, require_integer, run_logged,
|
|
19
|
-
without_digests, write_json_atomic,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
_probe = probe_json
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _asset(value, audio=False):
|
|
27
|
-
value = without_digests(value, 'asset')
|
|
28
|
-
require_fields(value, ['path', 'selected_stream'] if audio else ['path'], 'asset')
|
|
29
|
-
path = require_declared_path(value, 'asset')
|
|
30
|
-
if audio:
|
|
31
|
-
return {'path': str(path),
|
|
32
|
-
'selected_stream': require_integer(value['selected_stream'], 'Audio selected_stream')}
|
|
33
|
-
return {'path': str(path)}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _time(value):
|
|
37
|
-
if value in (None, 'N/A'):
|
|
38
|
-
raise ValueError('Missing media timing')
|
|
39
|
-
try:
|
|
40
|
-
return Fraction(str(value))
|
|
41
|
-
except (ValueError, ZeroDivisionError) as exc:
|
|
42
|
-
raise ValueError('Invalid media timing') from exc
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def probe_picture(path):
|
|
46
|
-
"""Actual full CFR presentation clock plus codec-order packet sizes and timestamps."""
|
|
47
|
-
data = _probe(path, '-select_streams', 'v:0', '-show_streams', '-show_packets',
|
|
48
|
-
'-show_format', '-show_entries',
|
|
49
|
-
'format=format_name:stream=codec_name,profile,level,width,height,pix_fmt,'
|
|
50
|
-
'sample_aspect_ratio,field_order,color_range,color_space,color_transfer,'
|
|
51
|
-
'color_primaries,chroma_location,time_base,start_pts,duration_ts,avg_frame_rate'
|
|
52
|
-
':packet=pts,dts,duration,size,side_data_list')
|
|
53
|
-
streams = data.get('streams', [])
|
|
54
|
-
if len(streams) != 1 or streams[0].get('codec_name') not in SUPPORTED_PICTURE_CODECS:
|
|
55
|
-
raise ValueError('Pairing requires one selected H264/HEVC picture stream')
|
|
56
|
-
if 'mp4' not in data.get('format', {}).get('format_name', '').split(','):
|
|
57
|
-
raise ValueError('Pairing currently requires MP4-family picture container')
|
|
58
|
-
v = streams[0]
|
|
59
|
-
fps, tb = _time(v.get('avg_frame_rate')), _time(v.get('time_base'))
|
|
60
|
-
if not 1 <= fps <= 120 or tb <= 0 or v.get('start_pts') != 0:
|
|
61
|
-
raise ValueError('Picture requires positive CFR and a known zero start')
|
|
62
|
-
duration = _time(v.get('duration_ts')) * tb
|
|
63
|
-
frames = _probe(path, '-select_streams', 'v:0', '-show_frames',
|
|
64
|
-
'-show_entries', 'frame=pts')['frames']
|
|
65
|
-
pts = [_time(frame.get('pts')) * tb for frame in frames]
|
|
66
|
-
if not pts or any(t != Fraction(i, fps) for i, t in enumerate(pts)) or duration != len(pts) / fps:
|
|
67
|
-
raise ValueError('Picture requires complete zero-origin CFR frame clock and exact duration')
|
|
68
|
-
decoder_keys = ['codec_name', 'profile', 'level', 'width', 'height', 'pix_fmt',
|
|
69
|
-
'sample_aspect_ratio', 'field_order', 'color_range', 'color_space',
|
|
70
|
-
'color_transfer', 'color_primaries', 'chroma_location']
|
|
71
|
-
packets = []
|
|
72
|
-
for packet in data.get('packets', []):
|
|
73
|
-
if packet.get('size') is None:
|
|
74
|
-
raise ValueError('Picture packet missing size')
|
|
75
|
-
ticks = {key: str(_time(packet.get(key)) * tb) for key in ['pts', 'dts', 'duration']}
|
|
76
|
-
if _time(ticks['duration']) <= 0:
|
|
77
|
-
raise ValueError('Picture packet has invalid duration')
|
|
78
|
-
packets.append({**ticks, 'size': int(packet['size']),
|
|
79
|
-
'side_data_list': packet.get('side_data_list', [])})
|
|
80
|
-
if len(packets) != len(pts):
|
|
81
|
-
raise ValueError('Picture packet/frame counts disagree')
|
|
82
|
-
return {'decoder': {key: v.get(key) for key in decoder_keys}, 'packets': packets,
|
|
83
|
-
'frame_pts': [str(t) for t in pts], 'frame_count': len(pts), 'fps': str(fps),
|
|
84
|
-
'duration': str(duration), 'start': '0'}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def validate_aac_packet_interval(audio):
|
|
88
|
-
"""Validate exact AAC packet continuity against the stream-header interval."""
|
|
89
|
-
if audio['codec'] != 'aac':
|
|
90
|
-
raise ValueError('Pairing requires AAC adopted audio')
|
|
91
|
-
packets = audio['packets']
|
|
92
|
-
rate = audio['sample_rate']
|
|
93
|
-
if type(rate) is not int or rate <= 0 or len(packets) < 2:
|
|
94
|
-
raise ValueError('AAC packet timing is incomplete')
|
|
95
|
-
nominal = _time(packets[0]['duration'])
|
|
96
|
-
# Accept known AAC frame sample counts, never an arbitrary huge duration.
|
|
97
|
-
if nominal * rate not in {960, 1024, 1920, 2048}:
|
|
98
|
-
raise ValueError('Unsupported AAC nominal packet duration')
|
|
99
|
-
for i, packet in enumerate(packets):
|
|
100
|
-
d = _time(packet['duration'])
|
|
101
|
-
if d <= 0 or d > nominal or (i < len(packets) - 1 and d != nominal):
|
|
102
|
-
raise ValueError('Nonuniform or invalid AAC packet duration')
|
|
103
|
-
p, t = _time(packet['pts']), _time(packet['dts'])
|
|
104
|
-
if p != t:
|
|
105
|
-
raise ValueError('Unsupported AAC PTS/DTS difference')
|
|
106
|
-
if i and p != _time(packets[i-1]['pts']) + _time(packets[i-1]['duration']):
|
|
107
|
-
raise ValueError('AAC packet clock has gaps or overlaps')
|
|
108
|
-
audio_start, audio_duration = _time(audio['start_time']), _time(audio['duration'])
|
|
109
|
-
if audio_duration <= 0:
|
|
110
|
-
raise ValueError('Invalid audio interval')
|
|
111
|
-
audio_end = audio_start + audio_duration
|
|
112
|
-
first_pts = _time(packets[0]['pts'])
|
|
113
|
-
last_end = _time(packets[-1]['pts']) + _time(packets[-1]['duration'])
|
|
114
|
-
if (not 0 <= audio_start - first_pts <= nominal
|
|
115
|
-
or abs(last_end - audio_end) > Fraction(1, rate)):
|
|
116
|
-
raise ValueError('AAC packet clock does not match stream interval')
|
|
117
|
-
return audio_start, audio_end, nominal
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def validate_pair_timing(picture, audio):
|
|
121
|
-
"""Narrow AAC/CFR compatibility; not a perceptual synchronization verdict."""
|
|
122
|
-
audio_start, audio_end, nominal = validate_aac_packet_interval(audio)
|
|
123
|
-
tolerance = max(1 / Fraction(picture['fps']), nominal)
|
|
124
|
-
end_delta = audio_end - Fraction(picture['duration'])
|
|
125
|
-
if abs(audio_start) > tolerance or abs(end_delta) > tolerance:
|
|
126
|
-
raise ValueError('Picture/audio interval mismatch; no implicit trim, padding, offset or retime')
|
|
127
|
-
return {'start_delta': str(audio_start),
|
|
128
|
-
'end_delta': str(end_delta),
|
|
129
|
-
'tolerance': str(tolerance), 'nominal_aac_packet': str(nominal)}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _run_mux(command, directory):
|
|
133
|
-
run_logged(command, directory, 'mux', timeout=600)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _verify_output(path):
|
|
137
|
-
data = _probe(path, '-show_streams')
|
|
138
|
-
if [stream.get('codec_type') for stream in data['streams']] != ['video', 'audio']:
|
|
139
|
-
raise ValueError('Paired output must contain only v:0 and a:0')
|
|
140
|
-
result = subprocess.run(['ffmpeg', '-v', 'error', '-xerror', '-threads', '2', '-i', str(path),
|
|
141
|
-
'-map', '0:v:0', '-map', '0:a:0', '-f', 'null', '-'],
|
|
142
|
-
capture_output=True, text=True, timeout=600)
|
|
143
|
-
if result.returncode or result.stderr.strip():
|
|
144
|
-
raise ValueError('Paired output full decode failed')
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def run_pair(plan_path, output_dir, *, plan_only=False):
|
|
148
|
-
plan_path, directory = Path(plan_path).resolve(), Path(output_dir).resolve()
|
|
149
|
-
directory.mkdir(parents=True, exist_ok=False)
|
|
150
|
-
report_path = directory / 'pair_run.json'
|
|
151
|
-
staged, output = directory / 'paired.rendering.mp4', directory / 'paired.mp4'
|
|
152
|
-
report = {'artifact': 'media_pair_run', 'schema_version': 1, 'status': 'PREPARING',
|
|
153
|
-
'direct_listening': 'NOT_CHECKED', 'normal_speed_review': 'NOT_CHECKED',
|
|
154
|
-
'release_approved': False}
|
|
155
|
-
write_json_atomic(report_path, report)
|
|
156
|
-
try:
|
|
157
|
-
plan = json.loads(plan_path.read_bytes())
|
|
158
|
-
require_fields(plan, ['artifact', 'schema_version', 'picture', 'audio'], 'media_pair plan')
|
|
159
|
-
if plan['artifact'] != 'media_pair' or type(plan['schema_version']) is not int or plan['schema_version'] != 1:
|
|
160
|
-
raise ValueError('Unsupported media_pair schema')
|
|
161
|
-
picture, audio = _asset(plan['picture']), _asset(plan['audio'], audio=True)
|
|
162
|
-
report.update(plan={'path': str(plan_path)}, inputs={'picture': picture, 'audio': audio})
|
|
163
|
-
video_facts = probe_picture(picture['path'])
|
|
164
|
-
audio_facts = probe_audio_packets(audio['path'], audio['selected_stream'])
|
|
165
|
-
report['timing'] = validate_pair_timing(video_facts, audio_facts)
|
|
166
|
-
report['picture'] = {k: v for k, v in video_facts.items() if k not in ['packets', 'frame_pts']}
|
|
167
|
-
report['audio'] = {'input_stream': audio['selected_stream'], 'output_stream': 0,
|
|
168
|
-
'packet_count': audio_facts['packet_count']}
|
|
169
|
-
if plan_only:
|
|
170
|
-
report['status'] = 'PLANNED'
|
|
171
|
-
write_json_atomic(report_path, report)
|
|
172
|
-
return report
|
|
173
|
-
command = ['ffmpeg', '-nostdin', '-v', 'error', '-n', '-copyts',
|
|
174
|
-
'-i', picture['path'], '-i', audio['path'], '-map', '0:v:0',
|
|
175
|
-
'-map', f"1:a:{audio['selected_stream']}", '-c', 'copy',
|
|
176
|
-
'-movie_timescale', str(audio_facts['sample_rate']),
|
|
177
|
-
'-movflags', '+faststart', str(staged)]
|
|
178
|
-
_run_mux(command, directory)
|
|
179
|
-
if probe_picture(staged) != video_facts:
|
|
180
|
-
raise ValueError('Paired picture packets, decoder, geometry/color or full frame clock changed')
|
|
181
|
-
proof = verify_adopted_audio(audio['path'], staged, audio['selected_stream'], 0)
|
|
182
|
-
output_timing = validate_pair_timing(video_facts, proof['output'])
|
|
183
|
-
if output_timing != report['timing']:
|
|
184
|
-
raise ValueError('Output audio presentation interval changed')
|
|
185
|
-
report['output_timing'] = output_timing
|
|
186
|
-
_verify_output(staged)
|
|
187
|
-
write_json_atomic(directory / 'picture_identity.json', video_facts)
|
|
188
|
-
write_json_atomic(directory / 'adopted_audio_identity.json', proof)
|
|
189
|
-
report['output'] = {'path': str(output), 'full_decode': 'PASS',
|
|
190
|
-
'picture_identity': 'EXACT', 'audio_packet_identity': 'EXACT'}
|
|
191
|
-
staged.rename(output)
|
|
192
|
-
report['status'] = 'PAIR_RENDERED'
|
|
193
|
-
write_json_atomic(report_path, report)
|
|
194
|
-
return report
|
|
195
|
-
except Exception as exc:
|
|
196
|
-
# A partial unique run is evidence, never a final/current asset. Keep logs.
|
|
197
|
-
staged.unlink(missing_ok=True)
|
|
198
|
-
output.unlink(missing_ok=True)
|
|
199
|
-
report.pop('output', None)
|
|
200
|
-
report.update(status='FAILED', error=f'{type(exc).__name__}: {exc}')
|
|
201
|
-
write_json_atomic(report_path, report)
|
|
202
|
-
raise
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def main():
|
|
206
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
207
|
-
parser.add_argument('plan')
|
|
208
|
-
parser.add_argument('--output-dir', required=True)
|
|
209
|
-
parser.add_argument('--plan-only', action='store_true')
|
|
210
|
-
args = parser.parse_args()
|
|
211
|
-
report = run_pair(args.plan, args.output_dir, plan_only=args.plan_only)
|
|
212
|
-
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if __name__ == '__main__':
|
|
216
|
-
main()
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# 前置 QC 数据契约
|
|
2
|
-
|
|
3
|
-
`final_qc.json`、`golden_eval.json`、`mimo_qc.json` 与 `preflight_qc.json` 共用一套最小结构,由本技能的 `scripts/qc_contract.py` 实现。
|
|
4
|
-
|
|
5
|
-
## 字段契约
|
|
6
|
-
|
|
7
|
-
- `schema_version`:整数 `1`。
|
|
8
|
-
- `artifact`:只能是 `final_qc.json`、`golden_eval.json`、`mimo_qc.json` 或 `preflight_qc.json`。
|
|
9
|
-
- `stage`:只能是 `pre_cut`、`post_cut`、`pre_tts`、`post_tts`、`pre_assemble`、`post_render`、`golden`。
|
|
10
|
-
- 不得使用 `pre_voiceover`、`post_voiceover`、`pre_export`、`post_export`、`final`、`golden_eval` 或 `mimo_qc` 作为阶段值。
|
|
11
|
-
- `mimo_qc.json` 是产物名;MiMo finding 必须挂在 `post_tts`、`pre_assemble` 等真实阶段上。
|
|
12
|
-
- `findings[]`:每项必须包含 `finding_id`、`stage`、`severity`、`blocking`、`deterministic`、`confidence`、`rule_id`、`decision_reason`、`location`、`evidence`、`sample_policy`、`model_used` 与 `next_action`。
|
|
13
|
-
- `sample_policy`:至少包含 `type`;其值只能是 `all`、`deterministic`、`sampled`、`semantic` 或 `aesthetic`。
|
|
14
|
-
- `location.timecode` 与 `location.source_span` 必须存在;剪辑前阶段可以把任一字段设为 `null`。
|
|
15
|
-
- 为兼容与诊断,当前辅助函数还可能写出 `id`、`category`、`code`、`message`、`source` 与 `objective_corroboration`。
|
|
16
|
-
|
|
17
|
-
## 阻断语义
|
|
18
|
-
|
|
19
|
-
只有确定性的客观规则可以阻断,例如产物缺失或过期、时长或媒体流错误、字幕与 TTS 放置错误、数据结构无效。
|
|
20
|
-
|
|
21
|
-
MiMo 语义/审美 finding 以及其他所有非确定性 finding **始终只给建议,不能阻断**。若存在客观佐证,必须由确定性生产者单独写成确定性 finding;它不能把主观模型观察升级成 blocker。运行时不存在 allow-list 或规则表逃生口。
|
|
22
|
-
|
|
23
|
-
`qc_contract.py` 只负责数据结构:不调用 MiMo、不连接流水线,也不自动修复。独立的 `mimo_qc.py` 只有在显式开启时,才会在 `pre_assemble` 和/或 `post_render` 各发起一次实时请求;它保持 fail-open,且不落盘凭证或抽帧 base64。所有报告构建器都通过 `qc_contract.redact_secrets` 处理元数据与证据:脱敏疑似密钥的键值,并从 URL 移除 userinfo、query 与 fragment,同时保留有用的 host/path 上下文。
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Public API and CLI entrypoint for advisory MiMo multimodal QC."""
|
|
3
|
-
|
|
4
|
-
import qc.mimo_runner as mimo_qc_runner
|
|
5
|
-
from qc.mimo_evidence import collect_evidence, safe_mimo_config
|
|
6
|
-
from qc.mimo_observations import normalize_observations
|
|
7
|
-
from qc.mimo_payload import build_payload
|
|
8
|
-
from qc.mimo_client import mimo_qc_api_call
|
|
9
|
-
|
|
10
|
-
clear_report = mimo_qc_runner.clear_report
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def run(work_dir, **kwargs):
|
|
14
|
-
"""mimo_qc_runner.run with the real MiMo transport bound at call time."""
|
|
15
|
-
return mimo_qc_runner.run(work_dir, api_call=mimo_qc_api_call, **kwargs)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def main(argv=None):
|
|
19
|
-
return mimo_qc_runner.main(argv, run_callable=run)
|
|
20
|
-
|
|
21
|
-
__all__ = [
|
|
22
|
-
"build_payload",
|
|
23
|
-
"clear_report",
|
|
24
|
-
"collect_evidence",
|
|
25
|
-
"main",
|
|
26
|
-
"mimo_qc_api_call",
|
|
27
|
-
"normalize_observations",
|
|
28
|
-
"run",
|
|
29
|
-
"safe_mimo_config",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
if __name__ == "__main__":
|
|
33
|
-
raise SystemExit(main())
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""MiMo QC subpackage."""
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"""Load this skill's MiMo client without depending on the ambient ``lib`` module."""
|
|
2
|
-
|
|
3
|
-
import importlib.util
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
_SPEC = importlib.util.spec_from_file_location(
|
|
7
|
-
"video_recap_mimo_qc_lib", Path(__file__).resolve().parents[1] / "lib.py"
|
|
8
|
-
)
|
|
9
|
-
_LIB = importlib.util.module_from_spec(_SPEC)
|
|
10
|
-
_SPEC.loader.exec_module(_LIB)
|
|
11
|
-
|
|
12
|
-
DEFAULT_CONFIG = _LIB.CONFIG
|
|
13
|
-
mimo_qc_api_call = _LIB.mimo_qc_api_call
|
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
"""Collect and redact local evidence for MiMo QC."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import os
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
|
-
from collections.abc import Mapping, Sequence
|
|
10
|
-
|
|
11
|
-
import qc_contract
|
|
12
|
-
from qc.mimo_client import DEFAULT_CONFIG
|
|
13
|
-
|
|
14
|
-
_JSON_ARTIFACTS = (
|
|
15
|
-
"narration.json",
|
|
16
|
-
"visual_overlays.json",
|
|
17
|
-
"clip_plan_validated.json",
|
|
18
|
-
"clip_plan.json",
|
|
19
|
-
"assembly_manifest.json",
|
|
20
|
-
"tts_meta.json",
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
_SOURCE_ASR_ARTIFACTS = (
|
|
24
|
-
"asr_clean.json",
|
|
25
|
-
"asr.json",
|
|
26
|
-
"asr_result.json",
|
|
27
|
-
"asr_segments.json",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
_GENERATED_SUBTITLE_ARTIFACTS = (
|
|
31
|
-
"subtitles.json",
|
|
32
|
-
"subtitle.json",
|
|
33
|
-
"subtitles.srt",
|
|
34
|
-
"subtitle.srt",
|
|
35
|
-
"subtitles.vtt",
|
|
36
|
-
"subtitle.vtt",
|
|
37
|
-
"output.srt",
|
|
38
|
-
"output.vtt",
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
_OPTIONAL_VISUAL_METADATA = (
|
|
42
|
-
"sampled_frames.json",
|
|
43
|
-
"frame_samples.json",
|
|
44
|
-
"storyboard.json",
|
|
45
|
-
"storyboard_meta.json",
|
|
46
|
-
"frames_manifest.json",
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _load_json(path: Path) -> Any:
|
|
51
|
-
return json.loads(path.read_text(encoding="utf-8"))
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _read_text_sample(path: Path, *, max_chars: int = 4000) -> dict[str, Any]:
|
|
55
|
-
text = path.read_text(encoding="utf-8", errors="replace")
|
|
56
|
-
return {
|
|
57
|
-
"kind": "text",
|
|
58
|
-
"bytes": path.stat().st_size,
|
|
59
|
-
"truncated": len(text) > max_chars,
|
|
60
|
-
"sample": text[:max_chars],
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _summarize(
|
|
65
|
-
value: Any, *, max_items: int = 8, max_string: int = 700, depth: int = 0
|
|
66
|
-
) -> Any:
|
|
67
|
-
"""Keep request/report evidence bounded while retaining useful structure."""
|
|
68
|
-
if depth >= 4:
|
|
69
|
-
return {"type": type(value).__name__, "omitted": True}
|
|
70
|
-
if isinstance(value, Mapping):
|
|
71
|
-
out = {
|
|
72
|
-
str(key): _summarize(
|
|
73
|
-
item, max_items=max_items, max_string=max_string, depth=depth + 1
|
|
74
|
-
)
|
|
75
|
-
for key, item in list(value.items())[:max_items]
|
|
76
|
-
}
|
|
77
|
-
if len(value) > max_items:
|
|
78
|
-
out["_omitted_keys"] = len(value) - max_items
|
|
79
|
-
return out
|
|
80
|
-
if isinstance(value, list):
|
|
81
|
-
return {
|
|
82
|
-
"count": len(value),
|
|
83
|
-
"items": [
|
|
84
|
-
_summarize(
|
|
85
|
-
item, max_items=max_items, max_string=max_string, depth=depth + 1
|
|
86
|
-
)
|
|
87
|
-
for item in value[:max_items]
|
|
88
|
-
],
|
|
89
|
-
"omitted": max(0, len(value) - max_items),
|
|
90
|
-
}
|
|
91
|
-
if isinstance(value, str) and len(value) > max_string:
|
|
92
|
-
return {"text": value[:max_string], "truncated": True, "chars": len(value)}
|
|
93
|
-
return value
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _collect_file(work_dir: Path, name: str) -> dict[str, Any] | None:
|
|
97
|
-
path = work_dir / name
|
|
98
|
-
if not path.is_file():
|
|
99
|
-
return None
|
|
100
|
-
if path.suffix.lower() == ".json":
|
|
101
|
-
kind, summary = "json", _summarize(_load_json(path))
|
|
102
|
-
else:
|
|
103
|
-
kind, summary = "text", _summarize(_read_text_sample(path))
|
|
104
|
-
stat = path.stat()
|
|
105
|
-
return {
|
|
106
|
-
"path": name,
|
|
107
|
-
"kind": kind,
|
|
108
|
-
"bytes": stat.st_size,
|
|
109
|
-
"mtime_ns": stat.st_mtime_ns,
|
|
110
|
-
"summary": summary,
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _first_existing(work_dir: Path, names: Sequence[str]) -> str | None:
|
|
115
|
-
return next((name for name in names if (work_dir / name).is_file()), None)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _collect_multi_source_asr(work_dir: Path) -> dict[str, Any]:
|
|
119
|
-
"""Collect per-source ASR when a multi-source project has no root transcript."""
|
|
120
|
-
manifest_path = work_dir / "multi_source_manifest.json"
|
|
121
|
-
if not manifest_path.is_file():
|
|
122
|
-
return {}
|
|
123
|
-
collected = {}
|
|
124
|
-
for source in _load_json(manifest_path)["sources"]:
|
|
125
|
-
relative_dir = source["source_work_dir"] # recap writes sources/<source_id>
|
|
126
|
-
name = _first_existing(work_dir / relative_dir, _SOURCE_ASR_ARTIFACTS)
|
|
127
|
-
if name is not None:
|
|
128
|
-
collected[source["source_id"]] = _collect_file(work_dir, f"{relative_dir}/{name}")
|
|
129
|
-
return collected
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _resolve_candidate(work_dir: Path, candidate: str | Path) -> Path:
|
|
133
|
-
path = Path(candidate)
|
|
134
|
-
return path if path.is_absolute() else work_dir / path
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _final_output_path(
|
|
138
|
-
work_dir: Path, final_output: str | Path | None
|
|
139
|
-
) -> tuple[Path, str] | None:
|
|
140
|
-
"""(path, display) of the final output: the caller's, else assembly_manifest.final_output
|
|
141
|
-
(video-assemble always writes it); None before the assembler has run."""
|
|
142
|
-
if final_output is None:
|
|
143
|
-
manifest_path = work_dir / "assembly_manifest.json"
|
|
144
|
-
if not manifest_path.is_file():
|
|
145
|
-
return None
|
|
146
|
-
final_output = _load_json(manifest_path)["final_output"]
|
|
147
|
-
return _resolve_candidate(work_dir, final_output), str(final_output)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _final_output_metadata(
|
|
151
|
-
work_dir: Path, final_output: str | Path | None = None
|
|
152
|
-
) -> dict[str, Any]:
|
|
153
|
-
resolved = _final_output_path(work_dir, final_output)
|
|
154
|
-
if resolved is None:
|
|
155
|
-
return {"candidates": []}
|
|
156
|
-
path, display = resolved
|
|
157
|
-
item: dict[str, Any] = {"path": display, "exists": path.is_file()}
|
|
158
|
-
if item["exists"]:
|
|
159
|
-
stat = path.stat()
|
|
160
|
-
item.update({"bytes": stat.st_size, "mtime_ns": stat.st_mtime_ns})
|
|
161
|
-
return {"candidates": [item]}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _existing_final_output(
|
|
165
|
-
work_dir: Path, final_output: str | Path | None
|
|
166
|
-
) -> Path | None:
|
|
167
|
-
resolved = _final_output_path(work_dir, final_output)
|
|
168
|
-
return resolved[0] if resolved is not None and resolved[0].is_file() else None
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def collect_evidence(
|
|
172
|
-
work_dir: str | Path, *, final_output: str | Path | None = None
|
|
173
|
-
) -> dict[str, Any]:
|
|
174
|
-
"""Collect lightweight, secret-scrubbed evidence from one work directory."""
|
|
175
|
-
root = Path(work_dir)
|
|
176
|
-
artifacts: dict[str, Any] = {}
|
|
177
|
-
preferred_plan = _first_existing(
|
|
178
|
-
root, ("clip_plan_validated.json", "clip_plan.json")
|
|
179
|
-
)
|
|
180
|
-
for name in _JSON_ARTIFACTS:
|
|
181
|
-
if (
|
|
182
|
-
name in {"clip_plan_validated.json", "clip_plan.json"}
|
|
183
|
-
and name != preferred_plan
|
|
184
|
-
):
|
|
185
|
-
continue
|
|
186
|
-
item = _collect_file(root, name)
|
|
187
|
-
if item is not None:
|
|
188
|
-
artifacts[name] = item
|
|
189
|
-
evidence = {
|
|
190
|
-
# Display only; excluded from cache_input so moving the work directory is a cache hit.
|
|
191
|
-
"work_dir": str(root),
|
|
192
|
-
"artifacts": artifacts,
|
|
193
|
-
"source_asr": {
|
|
194
|
-
name: item
|
|
195
|
-
for name in _SOURCE_ASR_ARTIFACTS
|
|
196
|
-
if (item := _collect_file(root, name)) is not None
|
|
197
|
-
},
|
|
198
|
-
"generated_subtitles": {
|
|
199
|
-
name: item
|
|
200
|
-
for name in _GENERATED_SUBTITLE_ARTIFACTS
|
|
201
|
-
if (item := _collect_file(root, name)) is not None
|
|
202
|
-
},
|
|
203
|
-
"visual_metadata": {
|
|
204
|
-
name: item
|
|
205
|
-
for name in _OPTIONAL_VISUAL_METADATA
|
|
206
|
-
if (item := _collect_file(root, name)) is not None
|
|
207
|
-
},
|
|
208
|
-
"final_output": _final_output_metadata(root, final_output),
|
|
209
|
-
}
|
|
210
|
-
if not evidence["source_asr"]:
|
|
211
|
-
evidence["source_asr"] = _collect_multi_source_asr(root)
|
|
212
|
-
# The evidence goes into the MiMo request as well as the persisted report, so it is
|
|
213
|
-
# redacted here, before either.
|
|
214
|
-
return qc_contract.redact_secrets(evidence)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _cache_file_group(group: Mapping[str, Any]) -> dict[str, Any]:
|
|
218
|
-
return {
|
|
219
|
-
name: {key: item[key] for key in ("kind", "bytes", "mtime_ns")}
|
|
220
|
-
for name, item in sorted(group.items())
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def _cache_evidence(evidence: Mapping[str, Any]) -> dict[str, Any]:
|
|
225
|
-
"""The per-file identities ({kind, bytes, mtime_ns}) a cached stage report was built
|
|
226
|
-
from; work_dir is excluded so moving the directory is still a cache hit."""
|
|
227
|
-
return {
|
|
228
|
-
"artifacts": _cache_file_group(evidence["artifacts"]),
|
|
229
|
-
"source_asr": _cache_file_group(evidence["source_asr"]),
|
|
230
|
-
"generated_subtitles": _cache_file_group(evidence["generated_subtitles"]),
|
|
231
|
-
"visual_metadata": _cache_file_group(evidence["visual_metadata"]),
|
|
232
|
-
"final_output": [
|
|
233
|
-
{key: item[key] for key in ("exists", "bytes", "mtime_ns") if key in item}
|
|
234
|
-
for item in evidence["final_output"]["candidates"]
|
|
235
|
-
],
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def _effective_config(config: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
|
240
|
-
source = dict(DEFAULT_CONFIG)
|
|
241
|
-
if config:
|
|
242
|
-
source.update(dict(config))
|
|
243
|
-
if not config.get("mimo_qc_model"):
|
|
244
|
-
source["mimo_qc_model"] = (
|
|
245
|
-
config.get("mimo_video_model")
|
|
246
|
-
or config.get("mimo_model")
|
|
247
|
-
or source["mimo_qc_model"]
|
|
248
|
-
)
|
|
249
|
-
# MIMO_QC_MODEL is intentionally read at call time for embedded/CLI tests and
|
|
250
|
-
# long-running agent processes whose environment may be adjusted between runs.
|
|
251
|
-
if os.environ.get("MIMO_QC_MODEL") and not (config and config.get("mimo_qc_model")):
|
|
252
|
-
source["mimo_qc_model"] = os.environ["MIMO_QC_MODEL"]
|
|
253
|
-
return source
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def safe_mimo_config(config: Mapping[str, Any] | None = None) -> dict[str, Any]:
|
|
257
|
-
"""Return non-secret settings suitable for persisted report metadata."""
|
|
258
|
-
source = _effective_config(config)
|
|
259
|
-
keep = (
|
|
260
|
-
"api_provider",
|
|
261
|
-
"mimo_api_url",
|
|
262
|
-
"mimo_api_url_source",
|
|
263
|
-
"mimo_video_api_url",
|
|
264
|
-
"mimo_video_api_url_source",
|
|
265
|
-
"mimo_qc_model",
|
|
266
|
-
"mimo_qc_model_source",
|
|
267
|
-
"mimo_model",
|
|
268
|
-
"mimo_model_source",
|
|
269
|
-
"mimo_video_model",
|
|
270
|
-
"mimo_video_model_source",
|
|
271
|
-
"mimo_disable_thinking",
|
|
272
|
-
"mimo_disable_thinking_source",
|
|
273
|
-
"mimo_media_resolution",
|
|
274
|
-
"mimo_media_resolution_source",
|
|
275
|
-
)
|
|
276
|
-
safe = {key: source[key] for key in keep}
|
|
277
|
-
safe["model"] = (
|
|
278
|
-
source["mimo_qc_model"] or source["mimo_video_model"] or source["mimo_model"]
|
|
279
|
-
)
|
|
280
|
-
key_present = bool(
|
|
281
|
-
source.get("mimo_video_api_key")
|
|
282
|
-
or source.get("mimo_api_key")
|
|
283
|
-
or source.get("api_key")
|
|
284
|
-
)
|
|
285
|
-
safe = qc_contract.redact_secrets(safe)
|
|
286
|
-
safe["key_present"] = key_present
|
|
287
|
-
return safe
|