@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,204 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"draft_cover_path": "",
|
|
3
|
-
"type": "combination",
|
|
4
|
-
"combination_id": "",
|
|
5
|
-
"draft_config_path": "",
|
|
6
|
-
"draft": {
|
|
7
|
-
"version": 360000,
|
|
8
|
-
"new_version": "111.0.0",
|
|
9
|
-
"source": "default",
|
|
10
|
-
"update_time": 0,
|
|
11
|
-
"keyframe_graph_list": [],
|
|
12
|
-
"duration": 86600000,
|
|
13
|
-
"uneven_animation_template_info": {
|
|
14
|
-
"order": "",
|
|
15
|
-
"sub_template_info_list": [],
|
|
16
|
-
"content": "",
|
|
17
|
-
"composition": ""
|
|
18
|
-
},
|
|
19
|
-
"tracks": [],
|
|
20
|
-
"canvas_config": {
|
|
21
|
-
"height": 1080,
|
|
22
|
-
"ratio": "original",
|
|
23
|
-
"width": 1920
|
|
24
|
-
},
|
|
25
|
-
"relationships": [],
|
|
26
|
-
"last_modified_platform": {
|
|
27
|
-
"app_version": "",
|
|
28
|
-
"os_version": "",
|
|
29
|
-
"os": "",
|
|
30
|
-
"hard_disk_id": "",
|
|
31
|
-
"device_id": "",
|
|
32
|
-
"mac_address": "",
|
|
33
|
-
"app_id": 0,
|
|
34
|
-
"app_source": ""
|
|
35
|
-
},
|
|
36
|
-
"color_space": -1,
|
|
37
|
-
"static_cover_image_path": "",
|
|
38
|
-
"render_index_track_mode_on": true,
|
|
39
|
-
"keyframes": {
|
|
40
|
-
"handwrites": [],
|
|
41
|
-
"videos": [],
|
|
42
|
-
"texts": [],
|
|
43
|
-
"audios": [],
|
|
44
|
-
"adjusts": [],
|
|
45
|
-
"stickers": [],
|
|
46
|
-
"filters": [],
|
|
47
|
-
"effects": []
|
|
48
|
-
},
|
|
49
|
-
"platform": {
|
|
50
|
-
"app_version": "9.6.0",
|
|
51
|
-
"os_version": "15.1",
|
|
52
|
-
"os": "mac",
|
|
53
|
-
"hard_disk_id": "9d5cea4f22458a4e59d643d07162d324",
|
|
54
|
-
"device_id": "0b02b9bd41947815545481af8a5bde46",
|
|
55
|
-
"mac_address": "f8e784e2995a6aed158ef584d6245be8",
|
|
56
|
-
"app_id": 3704,
|
|
57
|
-
"app_source": "lv"
|
|
58
|
-
},
|
|
59
|
-
"smart_ads_info": {
|
|
60
|
-
"page_from": "",
|
|
61
|
-
"routine": "",
|
|
62
|
-
"draft_url": ""
|
|
63
|
-
},
|
|
64
|
-
"path": "",
|
|
65
|
-
"id": "AFCFCFAC-468E-46B6-92DC-8F2C66411CA3",
|
|
66
|
-
"name": "",
|
|
67
|
-
"lyrics_effects": [],
|
|
68
|
-
"fps": 30,
|
|
69
|
-
"config": {
|
|
70
|
-
"use_float_render": false,
|
|
71
|
-
"adjust_max_index": 1,
|
|
72
|
-
"multi_language_list": [],
|
|
73
|
-
"multi_language_main": "none",
|
|
74
|
-
"lyrics_taskinfo": [],
|
|
75
|
-
"lyrics_recognition_id": "",
|
|
76
|
-
"system_font_list": [],
|
|
77
|
-
"sticker_max_index": 1,
|
|
78
|
-
"multi_language_mode": "none",
|
|
79
|
-
"material_save_mode": 0,
|
|
80
|
-
"attachment_info": [],
|
|
81
|
-
"video_mute": false,
|
|
82
|
-
"maintrack_adsorb": false,
|
|
83
|
-
"multi_language_current": "none",
|
|
84
|
-
"subtitle_sync": true,
|
|
85
|
-
"subtitle_recognition_id": "",
|
|
86
|
-
"combination_max_index": 1,
|
|
87
|
-
"extract_audio_last_index": 1,
|
|
88
|
-
"subtitle_taskinfo": [],
|
|
89
|
-
"record_audio_last_index": 1,
|
|
90
|
-
"lyrics_sync": true,
|
|
91
|
-
"original_sound_last_index": 1
|
|
92
|
-
},
|
|
93
|
-
"create_time": 0,
|
|
94
|
-
"draft_type": "video",
|
|
95
|
-
"function_assistant_info": {
|
|
96
|
-
"auto_adjust": false,
|
|
97
|
-
"auto_caption": false,
|
|
98
|
-
"enhande_voice": false,
|
|
99
|
-
"auto_adjust_fixed": false,
|
|
100
|
-
"fixed_rec_applied": false,
|
|
101
|
-
"retouch_segid_list": [],
|
|
102
|
-
"retouch": false,
|
|
103
|
-
"enhande_voice_fixed": false,
|
|
104
|
-
"enhance_quality_segid_list": [],
|
|
105
|
-
"deflicker_segid_list": [],
|
|
106
|
-
"auto_caption_template_id": "",
|
|
107
|
-
"color_correction": false,
|
|
108
|
-
"caption_opt": false,
|
|
109
|
-
"audio_noise_segid_list": [],
|
|
110
|
-
"auto_caption_segid_list": [],
|
|
111
|
-
"normalize_loudness_fixed": false,
|
|
112
|
-
"normalize_loudness_segid_list": [],
|
|
113
|
-
"eye_correction": false,
|
|
114
|
-
"enhance_quality_fixed": false,
|
|
115
|
-
"color_correction_fixed_value": 50,
|
|
116
|
-
"normalize_loudness_audio_denoise_segid_list": [],
|
|
117
|
-
"video_noise_segid_list": [],
|
|
118
|
-
"eye_correction_segid_list": [],
|
|
119
|
-
"retouch_fixed": false,
|
|
120
|
-
"auto_adjust_segid_list": [],
|
|
121
|
-
"enhance_voice_segid_list": [],
|
|
122
|
-
"smooth_slow_motion_fixed": false,
|
|
123
|
-
"caption_opt_segid_list": [],
|
|
124
|
-
"color_correction_segid_list": [],
|
|
125
|
-
"smooth_slow_motion": false,
|
|
126
|
-
"smart_segid_list": [],
|
|
127
|
-
"smart_rec_applied": false,
|
|
128
|
-
"color_correction_fixed": false,
|
|
129
|
-
"fps": {
|
|
130
|
-
"den": 1,
|
|
131
|
-
"num": 0
|
|
132
|
-
},
|
|
133
|
-
"auto_adjust_fixed_value": 50,
|
|
134
|
-
"normalize_loudness": false,
|
|
135
|
-
"enhance_quality": false
|
|
136
|
-
},
|
|
137
|
-
"materials": {
|
|
138
|
-
"audio_fades": [],
|
|
139
|
-
"images": [],
|
|
140
|
-
"flowers": [],
|
|
141
|
-
"smart_crops": [],
|
|
142
|
-
"common_mask": [],
|
|
143
|
-
"time_marks": [],
|
|
144
|
-
"shapes": [],
|
|
145
|
-
"vocal_beautifys": [],
|
|
146
|
-
"plugin_effects": [],
|
|
147
|
-
"audios": [],
|
|
148
|
-
"smart_relights": [],
|
|
149
|
-
"video_radius": [],
|
|
150
|
-
"vocal_separations": [],
|
|
151
|
-
"video_effects": [],
|
|
152
|
-
"video_shadows": [],
|
|
153
|
-
"videos": [],
|
|
154
|
-
"manual_deformations": [],
|
|
155
|
-
"video_strokes": [],
|
|
156
|
-
"audio_effects": [],
|
|
157
|
-
"realtime_denoises": [],
|
|
158
|
-
"handwrites": [],
|
|
159
|
-
"texts": [],
|
|
160
|
-
"text_templates": [],
|
|
161
|
-
"hsl_curves": [],
|
|
162
|
-
"tail_leaders": [],
|
|
163
|
-
"manual_beautys": [],
|
|
164
|
-
"material_colors": [],
|
|
165
|
-
"multi_language_refs": [],
|
|
166
|
-
"material_animations": [],
|
|
167
|
-
"chromas": [],
|
|
168
|
-
"speeds": [],
|
|
169
|
-
"audio_pannings": [],
|
|
170
|
-
"sound_channel_mappings": [],
|
|
171
|
-
"ai_translates": [],
|
|
172
|
-
"video_trackings": [],
|
|
173
|
-
"primary_color_wheels": [],
|
|
174
|
-
"placeholders": [],
|
|
175
|
-
"stickers": [],
|
|
176
|
-
"placeholder_infos": [],
|
|
177
|
-
"beats": [],
|
|
178
|
-
"audio_pitch_shifts": [],
|
|
179
|
-
"color_curves": [],
|
|
180
|
-
"green_screens": [],
|
|
181
|
-
"drafts": [],
|
|
182
|
-
"digital_humans": [],
|
|
183
|
-
"effects": [],
|
|
184
|
-
"digital_human_model_dressing": [],
|
|
185
|
-
"audio_balances": [],
|
|
186
|
-
"hsl": [],
|
|
187
|
-
"log_color_wheels": [],
|
|
188
|
-
"canvases": [],
|
|
189
|
-
"loudnesses": [],
|
|
190
|
-
"transitions": [],
|
|
191
|
-
"audio_track_indexes": []
|
|
192
|
-
},
|
|
193
|
-
"is_drop_frame_timecode": false,
|
|
194
|
-
"free_render_index_mode_on": false
|
|
195
|
-
},
|
|
196
|
-
"category_id": "",
|
|
197
|
-
"precompile_combination": false,
|
|
198
|
-
"combination_type": "none",
|
|
199
|
-
"formula_id": "",
|
|
200
|
-
"name": "",
|
|
201
|
-
"id": "",
|
|
202
|
-
"draft_file_path": "",
|
|
203
|
-
"category_name": ""
|
|
204
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# Pair rebuilt picture with a retained adopted soundtrack
|
|
2
|
-
|
|
3
|
-
When the picture is rebuilt/reframed but the adopted mix must not change, use
|
|
4
|
-
`pair_media.py` before `assemble.py --audio-mode adopted-packet-copy`. This is a
|
|
5
|
-
real two-input stream copy, not re-encoding an old finished video's picture.
|
|
6
|
-
It does not generate narration, align speech, design packaging, or create an
|
|
7
|
-
end card. It does not decide which inputs the editor intended to adopt.
|
|
8
|
-
|
|
9
|
-
```json
|
|
10
|
-
{
|
|
11
|
-
"artifact": "media_pair",
|
|
12
|
-
"schema_version": 1,
|
|
13
|
-
"picture": {"path": "/project/picture.mp4"},
|
|
14
|
-
"audio": {"path": "/project/adopted.m4a", "selected_stream": 0}
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Both paths must be explicit local files. `selected_stream` is the audio ordinal
|
|
19
|
-
(`a:N`), not the absolute stream index. The audio donor may be an old MP4 with
|
|
20
|
-
unrelated video: only the selected audio is used. The picture's audio is ignored.
|
|
21
|
-
Both files must exist when the plan is read. The plan is strict: unknown fields
|
|
22
|
-
do not silently request unsupported retime, gain, trimming or offset operations
|
|
23
|
-
(legacy `sha256` keys are ignored).
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
python3 scripts/pair_media.py pair.json --output-dir new-pair-directory --plan-only
|
|
27
|
-
# A PLANNED directory is not reusable as a rendered candidate: choose a NEW one.
|
|
28
|
-
python3 scripts/pair_media.py pair.json --output-dir new-render-directory
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The output directory must not already exist. No existing input, final asset,
|
|
32
|
-
current pointer, or user-approved version is overwritten. `--plan-only` checks
|
|
33
|
-
actual inputs and timing but creates no video and invokes no mux. A rendered run
|
|
34
|
-
provides:
|
|
35
|
-
|
|
36
|
-
- `paired.mp4`: selected picture and audio, both compressed-stream copied.
|
|
37
|
-
- `pair_run.json`: `PLANNED`, `PAIR_RENDERED`, or `FAILED`, explicit input and
|
|
38
|
-
plan/output paths, frame count, timing tolerance and output stream numbering.
|
|
39
|
-
- `picture_identity.json`: decoder, geometry/color, complete actual frame clock
|
|
40
|
-
and compressed packet size/timing/side-data facts.
|
|
41
|
-
- `adopted_audio_identity.json`: donor and output AAC packet/decoder facts.
|
|
42
|
-
- mux command/log for reconstruction and diagnosis.
|
|
43
|
-
|
|
44
|
-
The first implementation deliberately accepts H264 or HEVC in an MP4-family container,
|
|
45
|
-
complete zero-origin CFR picture (1–120 fps), and contiguous AAC packets with
|
|
46
|
-
known nominal sample duration. No VFR, retime, offset, format conversion, padding,
|
|
47
|
-
trimming, `-shortest`, normalization or gain is inferred. Packet priming and skip
|
|
48
|
-
metadata are retained. The packet clock must also agree with the declared audio
|
|
49
|
-
stream interval (at most one nominal priming packet before the start; the packet
|
|
50
|
-
end matches the stream end within one sample). This is checked again on the
|
|
51
|
-
actual muxed output. Picture/audio starts and ends must differ by no more than
|
|
52
|
-
the larger of one picture frame or one nominal AAC packet. The interval check is
|
|
53
|
-
compatibility, **not perceptual synchronization or acoustic alignment**. A tiny
|
|
54
|
-
container-tail tolerance does not authorize cutting a word.
|
|
55
|
-
|
|
56
|
-
Muxing goes to a staging file. The output's video decoder/packet sizes and
|
|
57
|
-
timestamps/full frame clock/geometry/color and adopted AAC packets must match
|
|
58
|
-
the inputs, the output must contain only `v:0,a:0`, and full decode must pass
|
|
59
|
-
before final publication. Failure leaves a FAILED record and logs, not a
|
|
60
|
-
final `paired.mp4`. Rebuilding source geometry is certified by its own upstream
|
|
61
|
-
render/map evidence, not by the existence of a successfully paired container.
|
|
62
|
-
|
|
63
|
-
## Subtitle integration: bind AFTER pairing
|
|
64
|
-
|
|
65
|
-
The output audio ordinal is always **0**, even when the donor used `a:1`.
|
|
66
|
-
Create the complete output-clock `subtitle_track.json` against
|
|
67
|
-
`subtitles.track_binding.current_bindings(paired_video, 0)`, then run the existing
|
|
68
|
-
adopted assembly path. Do not reuse a binding computed against the picture-only
|
|
69
|
-
file or the donor's old stream ordinal. A present stale subtitle track fails rather than falling
|
|
70
|
-
back to estimated timings. See [subtitle-track.md](subtitle-track.md).
|
|
71
|
-
|
|
72
|
-
Pairing preserves the input picture, including any explicit black tail; it does
|
|
73
|
-
not turn that black tail into a branded end card. Listening, normal-speed review,
|
|
74
|
-
editorial intent and release approval remain separate from this mechanical proof.
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Compose caller-rendered RGBA pixels over a frozen H264/CFR/AAC base.
|
|
3
|
-
|
|
4
|
-
This operation validates pixels and declared inputs. It does not generate or
|
|
5
|
-
interpret titles, dialogue, brands, typography, or release approval.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import argparse
|
|
9
|
-
from fractions import Fraction
|
|
10
|
-
import json
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
import subprocess
|
|
13
|
-
|
|
14
|
-
from adoption.frozen_audio import probe_audio_packets, verify_adopted_audio
|
|
15
|
-
from pair_media import probe_picture, validate_pair_timing
|
|
16
|
-
from adoption.strict_inputs import (
|
|
17
|
-
canonical_fraction, require_declared_path, require_fields, require_integer, run_logged,
|
|
18
|
-
without_digests, write_json_atomic,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
PATTERN = "frame_%06d.png"
|
|
23
|
-
ENCODING = {
|
|
24
|
-
"video_codec": "libx264", "preset": "fast", "crf": 18,
|
|
25
|
-
"pixel_format": "yuv420p", "color": "bt709_tv", "audio_codec": "copy",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _fields(value, required):
|
|
30
|
-
value = without_digests(value, "compose plan")
|
|
31
|
-
require_fields(value, required, "compose plan")
|
|
32
|
-
return value
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def _local_file(value, label):
|
|
36
|
-
_fields(value, ["path"])
|
|
37
|
-
return {"path": str(require_declared_path(value, label))}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _sequence_paths(directory, pattern, start, end):
|
|
41
|
-
if pattern != PATTERN:
|
|
42
|
-
raise ValueError(f"Only the literal simple pattern {PATTERN!r} is supported")
|
|
43
|
-
directory = Path(directory).resolve()
|
|
44
|
-
if not directory.is_dir():
|
|
45
|
-
raise FileNotFoundError(f"Sequence directory missing: {directory}")
|
|
46
|
-
expected = [directory / (pattern % index) for index in range(start, end)]
|
|
47
|
-
actual = sorted(directory.iterdir(), key=lambda item: item.name)
|
|
48
|
-
if [item.name for item in actual] != [item.name for item in expected]:
|
|
49
|
-
raise ValueError("Sequence requires exact contiguous files with no missing or extra entries")
|
|
50
|
-
if not all(path.is_file() for path in expected):
|
|
51
|
-
raise FileNotFoundError("Sequence frame missing or not a file")
|
|
52
|
-
return directory, expected
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def _probe_image(path):
|
|
56
|
-
result = subprocess.run(
|
|
57
|
-
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_streams",
|
|
58
|
-
"-show_entries", "stream=codec_name,width,height,pix_fmt", "-of", "json", str(path)],
|
|
59
|
-
capture_output=True, text=True, timeout=600,
|
|
60
|
-
)
|
|
61
|
-
if result.returncode or result.stderr.strip():
|
|
62
|
-
raise ValueError(f"Image probe failed: {result.stderr.strip()}")
|
|
63
|
-
streams = json.loads(result.stdout).get("streams", [])
|
|
64
|
-
if len(streams) != 1 or streams[0].get("codec_name") != "png":
|
|
65
|
-
raise ValueError("Foreground assets must be PNG images")
|
|
66
|
-
return streams[0]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def _validate_rgba(path, width, height):
|
|
70
|
-
image = _probe_image(path)
|
|
71
|
-
if (image.get("width"), image.get("height")) != (width, height):
|
|
72
|
-
raise ValueError("PNG must exactly match the full output canvas")
|
|
73
|
-
if image.get("pix_fmt") != "rgba":
|
|
74
|
-
raise ValueError("PNG must contain an actual RGBA pixel format")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _sequence(value, required, *, local_count, width, height):
|
|
78
|
-
value = _fields(value, required)
|
|
79
|
-
directory = value["directory"]
|
|
80
|
-
if not isinstance(directory, str) or not directory or "://" in directory:
|
|
81
|
-
raise ValueError("Sequence requires a local directory")
|
|
82
|
-
if value["pattern"] != PATTERN:
|
|
83
|
-
raise ValueError(f"Only the literal simple pattern {PATTERN!r} is supported")
|
|
84
|
-
resolved, paths = _sequence_paths(directory, value["pattern"], 0, local_count)
|
|
85
|
-
for path in paths:
|
|
86
|
-
_validate_rgba(path, width, height)
|
|
87
|
-
return {**value, "directory": str(resolved), "frame_count": len(paths)}, paths
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def validate_endcard(value, *, foreground_end, total_frames, width, height):
|
|
91
|
-
"""Validate the exact endcard union and return normalized data and bound paths."""
|
|
92
|
-
if not isinstance(value, dict) or value.get("kind") not in {"none", "still", "sequence"}:
|
|
93
|
-
raise ValueError("Endcard kind must be none, still, or sequence")
|
|
94
|
-
value = without_digests(value, "endcard")
|
|
95
|
-
if value["kind"] == "none":
|
|
96
|
-
_fields(value, ["kind"])
|
|
97
|
-
if foreground_end != total_frames:
|
|
98
|
-
raise ValueError("No endcard requires foreground to cover the full frame clock")
|
|
99
|
-
return {"kind": "none"}, []
|
|
100
|
-
if value["kind"] == "still":
|
|
101
|
-
required = ["kind", "path", "start_frame", "end_frame"]
|
|
102
|
-
else:
|
|
103
|
-
required = ["kind", "directory", "pattern", "start_frame", "end_frame"]
|
|
104
|
-
_fields(value, required)
|
|
105
|
-
end_start = require_integer(value["start_frame"], "endcard start_frame")
|
|
106
|
-
end_end = require_integer(value["end_frame"], "endcard end_frame", minimum=1)
|
|
107
|
-
if end_start >= end_end:
|
|
108
|
-
raise ValueError("Endcard interval must be non-empty")
|
|
109
|
-
if end_start != foreground_end or end_end != total_frames:
|
|
110
|
-
raise ValueError("Foreground and endcard must exactly partition the base frame clock")
|
|
111
|
-
if value["kind"] == "still":
|
|
112
|
-
asset = _local_file({"path": value["path"]}, "endcard")
|
|
113
|
-
_validate_rgba(asset["path"], width, height)
|
|
114
|
-
return {**value, **asset}, [Path(asset["path"])]
|
|
115
|
-
return _sequence(
|
|
116
|
-
value, required, local_count=end_end - end_start, width=width, height=height,
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def validate_plan(plan_path):
|
|
121
|
-
plan_path = Path(plan_path).resolve()
|
|
122
|
-
plan = json.loads(plan_path.read_bytes())
|
|
123
|
-
_fields(plan, ["artifact", "schema_version", "base", "video", "foreground",
|
|
124
|
-
"endcard", "producer_receipt"])
|
|
125
|
-
plan = {**plan, "video": _fields(plan["video"], ["fps", "width", "height", "total_frames"])}
|
|
126
|
-
if plan["artifact"] != "foreground_compose_plan" or type(plan["schema_version"]) is not int \
|
|
127
|
-
or plan["schema_version"] != 1:
|
|
128
|
-
raise ValueError("Unsupported foreground_compose_plan schema")
|
|
129
|
-
base = _local_file(plan["base"], "base")
|
|
130
|
-
receipt = _local_file(plan["producer_receipt"], "producer_receipt")
|
|
131
|
-
fps = canonical_fraction(plan["video"]["fps"], "video fps")
|
|
132
|
-
width = require_integer(plan["video"]["width"], "video width", minimum=1)
|
|
133
|
-
height = require_integer(plan["video"]["height"], "video height", minimum=1)
|
|
134
|
-
total = require_integer(plan["video"]["total_frames"], "video total_frames", minimum=1)
|
|
135
|
-
_fields(plan["foreground"], ["directory", "pattern", "start_frame", "end_frame"])
|
|
136
|
-
foreground_start = require_integer(plan["foreground"]["start_frame"], "foreground start_frame")
|
|
137
|
-
foreground_end = require_integer(plan["foreground"]["end_frame"], "foreground end_frame", minimum=1)
|
|
138
|
-
if foreground_start != 0 or foreground_end > total:
|
|
139
|
-
raise ValueError("Foreground must start at frame 0 and not exceed the frame clock")
|
|
140
|
-
foreground, foreground_paths = _sequence(
|
|
141
|
-
plan["foreground"], ["directory", "pattern", "start_frame", "end_frame"],
|
|
142
|
-
local_count=foreground_end, width=width, height=height,
|
|
143
|
-
)
|
|
144
|
-
normalized_endcard, endcard_paths = validate_endcard(
|
|
145
|
-
plan["endcard"], foreground_end=foreground_end, total_frames=total,
|
|
146
|
-
width=width, height=height,
|
|
147
|
-
)
|
|
148
|
-
picture = probe_picture(base["path"])
|
|
149
|
-
decoder = picture["decoder"]
|
|
150
|
-
required_color = {"codec_name": "h264", "pix_fmt": "yuv420p", "color_range": "tv",
|
|
151
|
-
"color_space": "bt709", "color_transfer": "bt709",
|
|
152
|
-
"color_primaries": "bt709"}
|
|
153
|
-
if any(decoder.get(key) != value for key, value in required_color.items()):
|
|
154
|
-
raise ValueError("Base must be H264 yuv420p with explicit BT.709 TV color metadata")
|
|
155
|
-
if (decoder.get("width"), decoder.get("height"), picture["frame_count"],
|
|
156
|
-
Fraction(picture["fps"])) != (width, height, total, fps):
|
|
157
|
-
raise ValueError("Declared fps/canvas/count does not match the actual base")
|
|
158
|
-
audio = probe_audio_packets(base["path"], 0)
|
|
159
|
-
validate_pair_timing(picture, audio)
|
|
160
|
-
return {
|
|
161
|
-
"plan_path": plan_path, "base": base,
|
|
162
|
-
"video": {"fps": str(fps), "width": width, "height": height, "total_frames": total},
|
|
163
|
-
"foreground": foreground, "foreground_paths": foreground_paths,
|
|
164
|
-
"endcard": normalized_endcard, "endcard_paths": endcard_paths,
|
|
165
|
-
"producer_receipt": receipt, "picture": picture, "audio": audio,
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _run_ffmpeg(command, directory):
|
|
170
|
-
run_logged(command, directory, "compose", timeout=600)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _verify_output(path, validated):
|
|
174
|
-
output_picture = probe_picture(path)
|
|
175
|
-
expected = validated["picture"]
|
|
176
|
-
for key in ("frame_pts", "frame_count", "fps", "duration", "start"):
|
|
177
|
-
if output_picture[key] != expected[key]:
|
|
178
|
-
raise ValueError(f"Output picture {key} differs from the base")
|
|
179
|
-
decoder_keys = ("codec_name", "width", "height", "pix_fmt", "sample_aspect_ratio",
|
|
180
|
-
"field_order", "color_range", "color_space", "color_transfer",
|
|
181
|
-
"color_primaries", "chroma_location")
|
|
182
|
-
if {key: output_picture["decoder"].get(key) for key in decoder_keys} != \
|
|
183
|
-
{key: expected["decoder"].get(key) for key in decoder_keys}:
|
|
184
|
-
raise ValueError("Output codec/canvas/color metadata differs from the base")
|
|
185
|
-
proof = verify_adopted_audio(validated["base"]["path"], path, 0, 0)
|
|
186
|
-
if validate_pair_timing(output_picture, proof["output"]) != \
|
|
187
|
-
validate_pair_timing(expected, validated["audio"]):
|
|
188
|
-
raise ValueError("Output audio interval changed")
|
|
189
|
-
result = subprocess.run(
|
|
190
|
-
["ffmpeg", "-v", "error", "-xerror", "-threads", "2", "-i", str(path),
|
|
191
|
-
"-map", "0:v:0", "-map", "0:a:0", "-f", "null", "-"],
|
|
192
|
-
capture_output=True, text=True, timeout=600,
|
|
193
|
-
)
|
|
194
|
-
if result.returncode or result.stderr.strip():
|
|
195
|
-
raise ValueError("Foreground output full decode failed")
|
|
196
|
-
return output_picture, proof
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def run_compose(plan_path, output_dir, *, plan_only=False):
|
|
200
|
-
directory = Path(output_dir).resolve()
|
|
201
|
-
directory.mkdir(parents=True, exist_ok=False)
|
|
202
|
-
report_path = directory / "foreground_run.json"
|
|
203
|
-
staged = directory / "foreground.rendering.mp4"
|
|
204
|
-
output = directory / "foreground.mp4"
|
|
205
|
-
report = {"artifact": "foreground_compose_run", "schema_version": 1,
|
|
206
|
-
"status": "PREPARING", "direct_listening": "NOT_CHECKED",
|
|
207
|
-
"normal_speed_review": "NOT_CHECKED", "release_approved": False,
|
|
208
|
-
"encoding": ENCODING}
|
|
209
|
-
write_json_atomic(report_path, report)
|
|
210
|
-
try:
|
|
211
|
-
value = validate_plan(plan_path)
|
|
212
|
-
report.update(
|
|
213
|
-
plan={"path": str(value["plan_path"])},
|
|
214
|
-
base=value["base"], video=value["video"], foreground=value["foreground"],
|
|
215
|
-
endcard=value["endcard"],
|
|
216
|
-
producer_receipt={**value["producer_receipt"],
|
|
217
|
-
"semantic_validation": "DECLARED_NOT_CHECKED"},
|
|
218
|
-
)
|
|
219
|
-
if plan_only:
|
|
220
|
-
report["status"] = "PLANNED"
|
|
221
|
-
write_json_atomic(report_path, report)
|
|
222
|
-
return report
|
|
223
|
-
fps = value["video"]["fps"]
|
|
224
|
-
command = ["ffmpeg", "-nostdin", "-v", "error", "-n", "-copyts",
|
|
225
|
-
"-i", value["base"]["path"], "-framerate", fps, "-start_number", "0",
|
|
226
|
-
"-i", str(Path(value["foreground"]["directory"]) / PATTERN)]
|
|
227
|
-
if value["endcard"]["kind"] == "none":
|
|
228
|
-
filters = (
|
|
229
|
-
"[0:v]format=rgb24[base];[1:v]format=rgba,setpts=PTS-STARTPTS[fg];"
|
|
230
|
-
"[base][fg]overlay=0:0:eof_action=pass:format=rgb,"
|
|
231
|
-
"scale=in_range=pc:out_range=tv:"
|
|
232
|
-
"in_color_matrix=bt709:out_color_matrix=bt709,format=yuv420p,"
|
|
233
|
-
f"trim=end_frame={value['video']['total_frames']}[outv]"
|
|
234
|
-
)
|
|
235
|
-
elif value["endcard"]["kind"] == "still":
|
|
236
|
-
command += ["-loop", "1", "-framerate", fps, "-i", value["endcard"]["path"]]
|
|
237
|
-
else:
|
|
238
|
-
command += ["-framerate", fps, "-start_number", "0", "-i",
|
|
239
|
-
str(Path(value["endcard"]["directory"]) / PATTERN)]
|
|
240
|
-
if value["endcard"]["kind"] != "none":
|
|
241
|
-
end_start = value["endcard"]["start_frame"]
|
|
242
|
-
end_offset = Fraction(end_start, 1) / Fraction(fps)
|
|
243
|
-
filters = (
|
|
244
|
-
f"[0:v]format=rgb24[base];[1:v]format=rgba,setpts=PTS-STARTPTS[fg];"
|
|
245
|
-
f"[2:v]format=rgba,setpts=PTS-STARTPTS+{end_offset.numerator}/"
|
|
246
|
-
f"{end_offset.denominator}/TB[end];"
|
|
247
|
-
f"[base][fg]overlay=0:0:eof_action=pass:format=rgb:"
|
|
248
|
-
f"enable='lt(n,{end_start})'[body];"
|
|
249
|
-
f"[body][end]overlay=0:0:eof_action=repeat:format=rgb:"
|
|
250
|
-
f"enable='gte(n,{end_start})',scale=in_range=pc:out_range=tv:"
|
|
251
|
-
"in_color_matrix=bt709:out_color_matrix=bt709,format=yuv420p,"
|
|
252
|
-
f"trim=end_frame={value['video']['total_frames']}[outv]"
|
|
253
|
-
)
|
|
254
|
-
command += ["-filter_complex", filters, "-map", "[outv]", "-map", "0:a:0",
|
|
255
|
-
"-c:v", ENCODING["video_codec"], "-preset", ENCODING["preset"],
|
|
256
|
-
"-crf", str(ENCODING["crf"]), "-threads", "2",
|
|
257
|
-
"-pix_fmt", ENCODING["pixel_format"],
|
|
258
|
-
"-color_range", "tv", "-colorspace", "bt709", "-color_primaries", "bt709",
|
|
259
|
-
"-color_trc", "bt709", "-c:a", "copy", "-movie_timescale",
|
|
260
|
-
str(value["audio"]["sample_rate"]), "-movflags", "+faststart",
|
|
261
|
-
str(staged)]
|
|
262
|
-
_run_ffmpeg(command, directory)
|
|
263
|
-
output_picture, audio_proof = _verify_output(staged, value)
|
|
264
|
-
write_json_atomic(directory / "picture_identity.json", output_picture)
|
|
265
|
-
write_json_atomic(directory / "adopted_audio_identity.json", audio_proof)
|
|
266
|
-
report["output"] = {"path": str(output),
|
|
267
|
-
"full_decode": "PASS", "frame_clock": "EXACT",
|
|
268
|
-
"audio_packet_identity": "EXACT"}
|
|
269
|
-
staged.rename(output)
|
|
270
|
-
report["status"] = "FOREGROUND_RENDERED"
|
|
271
|
-
write_json_atomic(report_path, report)
|
|
272
|
-
return report
|
|
273
|
-
except Exception as exc:
|
|
274
|
-
staged.unlink(missing_ok=True)
|
|
275
|
-
output.unlink(missing_ok=True)
|
|
276
|
-
report.pop("output", None)
|
|
277
|
-
report.update(status="FAILED", error=f"{type(exc).__name__}: {exc}")
|
|
278
|
-
write_json_atomic(report_path, report)
|
|
279
|
-
raise
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
def main():
|
|
283
|
-
parser = argparse.ArgumentParser(description=__doc__)
|
|
284
|
-
parser.add_argument("plan")
|
|
285
|
-
parser.add_argument("--output-dir", required=True)
|
|
286
|
-
parser.add_argument("--plan-only", action="store_true")
|
|
287
|
-
args = parser.parse_args()
|
|
288
|
-
report = run_compose(args.plan, args.output_dir, plan_only=args.plan_only)
|
|
289
|
-
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if __name__ == "__main__":
|
|
293
|
-
main()
|