@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
|
@@ -21,27 +21,8 @@ class DraftBuildContext:
|
|
|
21
21
|
total_us: int
|
|
22
22
|
new_id: NewIdFn
|
|
23
23
|
probe: ProbeFn
|
|
24
|
-
resource_packages: dict = field(default_factory=dict)
|
|
25
|
-
style_presets: dict = field(default_factory=dict)
|
|
26
24
|
materials: dict[str, list] = field(
|
|
27
|
-
default_factory=lambda: {
|
|
28
|
-
key: []
|
|
29
|
-
for key in (
|
|
30
|
-
"audios",
|
|
31
|
-
"chromas",
|
|
32
|
-
"common_mask",
|
|
33
|
-
"drafts",
|
|
34
|
-
"effects",
|
|
35
|
-
"masks",
|
|
36
|
-
"speeds",
|
|
37
|
-
"stickers",
|
|
38
|
-
"text_templates",
|
|
39
|
-
"texts",
|
|
40
|
-
"transitions",
|
|
41
|
-
"video_effects",
|
|
42
|
-
"videos",
|
|
43
|
-
)
|
|
44
|
-
}
|
|
25
|
+
default_factory=lambda: {"audios": [], "texts": [], "videos": []}
|
|
45
26
|
)
|
|
46
27
|
tracks: list[dict] = field(default_factory=list)
|
|
47
28
|
notes: list[str] = field(default_factory=list)
|
|
@@ -58,8 +39,6 @@ class DraftBuildContext:
|
|
|
58
39
|
total_us=us(timeline["duration"]),
|
|
59
40
|
new_id=new_id,
|
|
60
41
|
probe=probe,
|
|
61
|
-
resource_packages=timeline.get("resource_packages", {}),
|
|
62
|
-
style_presets=timeline.get("style_presets", {}),
|
|
63
42
|
)
|
|
64
43
|
|
|
65
44
|
def add_segment(self, kind, base_name, start_us, duration_us, segment):
|
|
@@ -14,9 +14,6 @@ _TEMPLATE_FILES = {
|
|
|
14
14
|
"text": "empty_yj_material_text.json",
|
|
15
15
|
"text_style": "empty_jy_text_styles.json",
|
|
16
16
|
"segment": "empty_jy_segment.json",
|
|
17
|
-
"draft": "empty_jy_draft.json",
|
|
18
|
-
"combination_segment": "empty_jy_combination_segment.json",
|
|
19
|
-
"combination_video": "empty_jy_combination_video_material.json",
|
|
20
17
|
"meta": "empty_draft_meta_info.json",
|
|
21
18
|
"meta_material": "empty_jy_meta_material_value.json",
|
|
22
19
|
}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
"""Timeline validation
|
|
1
|
+
"""Timeline validation at the JianYing adapter boundary."""
|
|
2
2
|
|
|
3
3
|
import copy
|
|
4
4
|
import math
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
CURRENT_SCHEMA_VERSION = 2
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# Hand-authoring fields the exporter once mapped to JianYing (speed, reverse,
|
|
9
|
+
# transitions, masks, LUTs, green screen, rich text, resource tracks). No pipeline
|
|
10
|
+
# stage emits them; they are rejected so an old hand-written timeline fails loudly
|
|
11
|
+
# instead of silently exporting a draft without the effect it asked for.
|
|
12
|
+
REMOVED_ITEM_FIELDS = (
|
|
13
|
+
"chroma", "compound", "flip", "green_background", "lut", "mask", "opacity",
|
|
14
|
+
"reverse", "reverse_path", "rotation_degrees", "speed", "style", "style_id",
|
|
15
|
+
"transition", "words",
|
|
16
|
+
)
|
|
17
|
+
REMOVED_ROOT_FIELDS = ("resource_packages", "style_presets")
|
|
18
|
+
|
|
11
19
|
|
|
12
20
|
def _error(path, expectation):
|
|
13
21
|
raise ValueError(f"invalid timeline {path}: {expectation}")
|
|
@@ -38,6 +46,12 @@ def _require_string(container, key, path):
|
|
|
38
46
|
return value
|
|
39
47
|
|
|
40
48
|
|
|
49
|
+
def _reject_removed(container, fields, path):
|
|
50
|
+
for field in fields:
|
|
51
|
+
if field in container:
|
|
52
|
+
_error(_field_path(path, field), "JianYing authoring extension is no longer supported")
|
|
53
|
+
|
|
54
|
+
|
|
41
55
|
def _validate_span(item, path, start_key="timeline_start", end_key="timeline_end"):
|
|
42
56
|
start = _require_number(item, start_key, path, minimum=0)
|
|
43
57
|
end = _require_number(item, end_key, path, minimum=0)
|
|
@@ -45,101 +59,30 @@ def _validate_span(item, path, start_key="timeline_start", end_key="timeline_end
|
|
|
45
59
|
_error(f"{path}.{end_key}", f"must be greater than {start_key}")
|
|
46
60
|
|
|
47
61
|
|
|
48
|
-
def
|
|
49
|
-
|
|
62
|
+
def _validate_item(item, path):
|
|
63
|
+
if not isinstance(item, dict):
|
|
64
|
+
_error(path, "must be an object")
|
|
65
|
+
_reject_removed(item, REMOVED_ITEM_FIELDS, path)
|
|
66
|
+
_validate_span(item, path)
|
|
67
|
+
for field in ("scale", "position"):
|
|
50
68
|
if field in item and not isinstance(item[field], dict):
|
|
51
69
|
_error(f"{path}.{field}", "must be an object")
|
|
52
70
|
|
|
53
71
|
|
|
54
|
-
def _validate_resources(resources, path):
|
|
55
|
-
if not isinstance(resources, list) or any(not isinstance(item, dict) for item in resources):
|
|
56
|
-
_error(path, "must contain source_path objects")
|
|
57
|
-
for index, item in enumerate(resources):
|
|
58
|
-
_require_string(item, "source_path", f"{path}[{index}]")
|
|
59
|
-
|
|
60
|
-
|
|
61
72
|
def _validate_video_clip(clip, path):
|
|
62
|
-
|
|
63
|
-
_error(path, "must be an object")
|
|
73
|
+
_validate_item(clip, path)
|
|
64
74
|
_require_string(clip, "source_path", path)
|
|
65
|
-
_validate_span(clip, path)
|
|
66
75
|
_validate_span(clip, path, "source_start", "source_end")
|
|
67
76
|
if "audio" in clip and not isinstance(clip["audio"], dict):
|
|
68
77
|
_error(f"{path}.audio", "must be an object")
|
|
69
|
-
if "speed" in clip:
|
|
70
|
-
speed = _require_number(clip, "speed", path)
|
|
71
|
-
if speed <= 0:
|
|
72
|
-
_error(f"{path}.speed", "must be greater than 0")
|
|
73
|
-
source_duration = float(clip["source_end"]) - float(clip["source_start"])
|
|
74
|
-
target_duration = float(clip["timeline_end"]) - float(clip["timeline_start"])
|
|
75
|
-
expected_source_duration = target_duration * float(speed)
|
|
76
|
-
if not math.isclose(source_duration, expected_source_duration, rel_tol=1e-6, abs_tol=1e-4):
|
|
77
|
-
_error(
|
|
78
|
-
path,
|
|
79
|
-
"source duration must equal target duration multiplied by speed "
|
|
80
|
-
f"({source_duration} != {target_duration} * {speed})",
|
|
81
|
-
)
|
|
82
|
-
if "reverse" in clip and not isinstance(clip["reverse"], bool):
|
|
83
|
-
_error(f"{path}.reverse", "must be a boolean")
|
|
84
|
-
# A reversed clip may omit reverse_path: export_timeline_to_jianying generates it.
|
|
85
|
-
if "reverse_path" in clip:
|
|
86
|
-
_require_string(clip, "reverse_path", path)
|
|
87
|
-
_validate_transform(clip, path)
|
|
88
|
-
for field in ("transition", "mask", "lut", "chroma"):
|
|
89
|
-
if field not in clip:
|
|
90
|
-
continue
|
|
91
|
-
spec = clip[field]
|
|
92
|
-
if isinstance(spec, dict):
|
|
93
|
-
if "resources" in spec:
|
|
94
|
-
_validate_resources(spec["resources"], f"{path}.{field}.resources")
|
|
95
|
-
elif not isinstance(spec, str):
|
|
96
|
-
_error(f"{path}.{field}", "must be an object or resource-package name")
|
|
97
|
-
if "compound" in clip and not isinstance(clip["compound"], bool):
|
|
98
|
-
_error(f"{path}.compound", "must be a boolean")
|
|
99
|
-
if clip.get("compound") or "green_background" in clip or "chroma" in clip:
|
|
100
|
-
# Any one of these makes the clip a green-screen compound, which needs both.
|
|
101
|
-
background = clip.get("green_background")
|
|
102
|
-
if not isinstance(background, dict):
|
|
103
|
-
_error(f"{path}.green_background", "must be a local media object")
|
|
104
|
-
_require_string(background, "source_path", f"{path}.green_background")
|
|
105
|
-
_validate_transform(background, f"{path}.green_background")
|
|
106
|
-
if "chroma" not in clip:
|
|
107
|
-
_error(f"{path}.chroma", "compound green-screen clips require a chroma object")
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _validate_resource_config(config, path):
|
|
111
|
-
if not isinstance(config, dict):
|
|
112
|
-
_error(path, "must be an object")
|
|
113
|
-
if not isinstance(config.get("main_config"), dict):
|
|
114
|
-
_error(f"{path}.main_config", "must be an object")
|
|
115
|
-
_validate_resources(config.get("resources", []), f"{path}.resources")
|
|
116
78
|
|
|
117
79
|
|
|
118
80
|
def _validate_segment(segment, path, kind):
|
|
119
|
-
|
|
120
|
-
_error(path, "must be an object")
|
|
121
|
-
_validate_span(segment, path)
|
|
122
|
-
_validate_transform(segment, path)
|
|
123
|
-
if "speed" in segment:
|
|
124
|
-
speed = _require_number(segment, "speed", path)
|
|
125
|
-
if speed <= 0:
|
|
126
|
-
_error(f"{path}.speed", "must be greater than 0")
|
|
81
|
+
_validate_item(segment, path)
|
|
127
82
|
if kind in {"audio", "image"}:
|
|
128
83
|
_require_string(segment, "source_path", path)
|
|
129
|
-
elif
|
|
84
|
+
elif not isinstance(segment.get("text"), str):
|
|
130
85
|
_error(f"{path}.text", "must be a string")
|
|
131
|
-
elif kind in RESOURCE_TRACK_KINDS:
|
|
132
|
-
sources = [key for key in ("material", "resource_config", "resource_package") if key in segment]
|
|
133
|
-
if len(sources) != 1:
|
|
134
|
-
_error(path, "must define exactly one of material, resource_config, or resource_package")
|
|
135
|
-
source = segment[sources[0]]
|
|
136
|
-
if sources[0] == "resource_package":
|
|
137
|
-
if not isinstance(source, str) or not source:
|
|
138
|
-
_error(f"{path}.resource_package", "must be a non-empty string")
|
|
139
|
-
elif sources[0] == "material" and not isinstance(source, dict):
|
|
140
|
-
_error(f"{path}.material", "must be an object")
|
|
141
|
-
elif sources[0] == "resource_config":
|
|
142
|
-
_validate_resource_config(source, f"{path}.resource_config")
|
|
143
86
|
|
|
144
87
|
|
|
145
88
|
def _validate_track(track, path):
|
|
@@ -157,7 +100,7 @@ def _validate_track(track, path):
|
|
|
157
100
|
_validate_video_clip(clip, f"{path}.clips[{index}]")
|
|
158
101
|
return
|
|
159
102
|
|
|
160
|
-
if kind in {"audio", "image", "text"}
|
|
103
|
+
if kind in {"audio", "image", "text"}:
|
|
161
104
|
segments = track.get("segments")
|
|
162
105
|
if not isinstance(segments, list):
|
|
163
106
|
_error(f"{path}.segments", "must be an array")
|
|
@@ -173,7 +116,7 @@ def _validate_track(track, path):
|
|
|
173
116
|
_error(f"{path}.kind", f"unsupported track kind {kind!r}")
|
|
174
117
|
|
|
175
118
|
|
|
176
|
-
def
|
|
119
|
+
def _validate(timeline):
|
|
177
120
|
canvas = timeline.get("canvas")
|
|
178
121
|
if not isinstance(canvas, dict):
|
|
179
122
|
_error("canvas", "must be an object")
|
|
@@ -186,13 +129,7 @@ def _validate_v2(timeline):
|
|
|
186
129
|
_error("canvas.fps", "must be greater than 0")
|
|
187
130
|
|
|
188
131
|
_require_number(timeline, "duration", "", minimum=0)
|
|
189
|
-
|
|
190
|
-
if not isinstance(resource_packages, dict):
|
|
191
|
-
_error("resource_packages", "must be an object")
|
|
192
|
-
for name, config in resource_packages.items():
|
|
193
|
-
_validate_resource_config(config, f"resource_packages.{name}")
|
|
194
|
-
if "style_presets" in timeline and not isinstance(timeline["style_presets"], dict):
|
|
195
|
-
_error("style_presets", "must be an object")
|
|
132
|
+
_reject_removed(timeline, REMOVED_ROOT_FIELDS, "")
|
|
196
133
|
tracks = timeline.get("tracks")
|
|
197
134
|
if not isinstance(tracks, list):
|
|
198
135
|
_error("tracks", "must be an array")
|
|
@@ -201,23 +138,19 @@ def _validate_v2(timeline):
|
|
|
201
138
|
|
|
202
139
|
|
|
203
140
|
def normalize_timeline(timeline):
|
|
204
|
-
"""Return a validated schema-v2
|
|
141
|
+
"""Return a validated copy of a schema-v2 timeline (the version timeline.py emits)."""
|
|
205
142
|
if not isinstance(timeline, dict):
|
|
206
143
|
_error("root", "must be an object")
|
|
207
144
|
schema_version = timeline.get("schema_version")
|
|
208
145
|
if not isinstance(schema_version, int) or isinstance(schema_version, bool):
|
|
209
|
-
_error("schema_version", "must be integer
|
|
210
|
-
if schema_version
|
|
146
|
+
_error("schema_version", f"must be integer {CURRENT_SCHEMA_VERSION}")
|
|
147
|
+
if schema_version != CURRENT_SCHEMA_VERSION:
|
|
211
148
|
raise ValueError(
|
|
212
149
|
f"unsupported timeline schema_version {schema_version}; "
|
|
213
|
-
f"supported
|
|
150
|
+
f"only {CURRENT_SCHEMA_VERSION} is supported (a v1 timeline only needs "
|
|
151
|
+
f"schema_version set to {CURRENT_SCHEMA_VERSION})"
|
|
214
152
|
)
|
|
215
153
|
|
|
216
154
|
normalized = copy.deepcopy(timeline)
|
|
217
|
-
|
|
218
|
-
# Schema v2 is an additive extension of v1 (local image tracks). The
|
|
219
|
-
# migration therefore preserves all authored v1 fields and only advances
|
|
220
|
-
# the version before applying the current contract.
|
|
221
|
-
normalized["schema_version"] = CURRENT_SCHEMA_VERSION
|
|
222
|
-
_validate_v2(normalized)
|
|
155
|
+
_validate(normalized)
|
|
223
156
|
return normalized
|
|
@@ -20,19 +20,11 @@ SEGMENT_RENDER_INDEX = 2
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
TRACK_LAYOUT_BANDS = {
|
|
23
|
-
"sound": TrackBand("sound", "audio", 10_000, "sound effects"),
|
|
24
23
|
"audio": TrackBand("audio", "audio", 20_000, "narration, music, and general audio"),
|
|
25
|
-
"green_screen": TrackBand("green_screen", "video", 30_000, "green-screen background"),
|
|
26
24
|
"video": TrackBand("video", "video", 40_000, "base video"),
|
|
27
25
|
"image": TrackBand("image", "video", 50_000, "image and photo overlays"),
|
|
28
|
-
"mask": TrackBand("mask", "video", 60_000, "masks"),
|
|
29
|
-
"effect": TrackBand("effect", "effect", 70_000, "video effects"),
|
|
30
|
-
"video_effect": TrackBand("video_effect", "effect", 70_000, "video effects"),
|
|
31
|
-
"face_effect": TrackBand("face_effect", "effect", 70_010, "face effects"),
|
|
32
|
-
"sticker": TrackBand("sticker", "sticker", 80_000, "stickers"),
|
|
33
26
|
"subtitle": TrackBand("subtitle", "text", 90_000, "subtitles"),
|
|
34
27
|
"text": TrackBand("text", "text", 100_000, "plain text"),
|
|
35
|
-
"text_template": TrackBand("text_template", "text", 110_000, "text templates"),
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
|
|
@@ -7,7 +7,6 @@ import shutil
|
|
|
7
7
|
import tempfile
|
|
8
8
|
import time
|
|
9
9
|
import uuid
|
|
10
|
-
import zipfile
|
|
11
10
|
|
|
12
11
|
from jianying.templates import template
|
|
13
12
|
|
|
@@ -37,19 +36,6 @@ def _resource_kind(material, materials_key):
|
|
|
37
36
|
return "video", "video"
|
|
38
37
|
|
|
39
38
|
|
|
40
|
-
RESOURCE_DIRECTORY_BY_MATERIALS_KEY = {
|
|
41
|
-
"chromas": "effect",
|
|
42
|
-
"common_mask": "mask",
|
|
43
|
-
"effects": "effect",
|
|
44
|
-
"masks": "mask",
|
|
45
|
-
"stickers": "sticker",
|
|
46
|
-
"texts": "text",
|
|
47
|
-
"text_templates": "text_template",
|
|
48
|
-
"transitions": "transition",
|
|
49
|
-
"video_effects": "effect",
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
39
|
def _unused_name(directory, basename, used):
|
|
54
40
|
"""Return a collision-free filename within one resource directory."""
|
|
55
41
|
name = basename
|
|
@@ -96,82 +82,13 @@ def _meta_value(material, relative_path, metetype, copied_path, timestamp_ms):
|
|
|
96
82
|
return value
|
|
97
83
|
|
|
98
84
|
|
|
99
|
-
def
|
|
100
|
-
"""Yield root and nested compound-draft material dictionaries."""
|
|
101
|
-
materials = content["materials"]
|
|
102
|
-
yield materials
|
|
103
|
-
for draft in materials["drafts"]:
|
|
104
|
-
yield from _material_sets(draft["draft"])
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _replace_value(value, old, new):
|
|
108
|
-
if isinstance(value, dict):
|
|
109
|
-
for key, item in value.items():
|
|
110
|
-
value[key] = _replace_value(item, old, new)
|
|
111
|
-
elif isinstance(value, list):
|
|
112
|
-
for index, item in enumerate(value):
|
|
113
|
-
value[index] = _replace_value(item, old, new)
|
|
114
|
-
elif isinstance(value, str):
|
|
115
|
-
if value == old:
|
|
116
|
-
return new
|
|
117
|
-
return value
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _replace_material_resource_path(material, materials_key, old, new):
|
|
121
|
-
"""Rewrite one declared resource, including the known rich-text JSON field."""
|
|
122
|
-
_replace_value(material, old, new)
|
|
123
|
-
if materials_key != "texts" or not isinstance(material.get("content"), str):
|
|
124
|
-
return
|
|
125
|
-
content = json.loads(material["content"])
|
|
126
|
-
_replace_value(content, old, new)
|
|
127
|
-
material["content"] = json.dumps(content, ensure_ascii=False, separators=(",", ":"))
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _safe_resource_target(target_path):
|
|
131
|
-
normalized = os.path.normpath(str(target_path).replace("\\", "/"))
|
|
132
|
-
if normalized in {"", "."} or os.path.isabs(normalized):
|
|
133
|
-
raise ValueError(f"invalid JianYing resource target_path: {target_path}")
|
|
134
|
-
if normalized == ".." or normalized.startswith("../"):
|
|
135
|
-
raise ValueError(f"JianYing resource target_path escapes package: {target_path}")
|
|
136
|
-
return normalized
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _extract_zip(source, destination):
|
|
140
|
-
os.makedirs(destination, exist_ok=False)
|
|
141
|
-
destination_real = os.path.realpath(destination)
|
|
142
|
-
with zipfile.ZipFile(source) as archive:
|
|
143
|
-
for member in archive.infolist():
|
|
144
|
-
member_path = os.path.realpath(os.path.join(destination, member.filename))
|
|
145
|
-
if os.path.commonpath((destination_real, member_path)) != destination_real:
|
|
146
|
-
raise ValueError(f"unsafe path in JianYing resource archive: {member.filename}")
|
|
147
|
-
archive.extractall(destination)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _copy_resource(source, resource_kind, resources_root, used, target_path=None):
|
|
85
|
+
def _copy_resource(source, resource_kind, resources_root, used):
|
|
151
86
|
resource_dir = os.path.join(resources_root, resource_kind)
|
|
152
87
|
os.makedirs(resource_dir, exist_ok=True)
|
|
153
|
-
|
|
154
|
-
if target_path is None:
|
|
155
|
-
basename = os.path.basename(source.rstrip(os.sep))
|
|
156
|
-
if is_zip:
|
|
157
|
-
basename = os.path.splitext(basename)[0]
|
|
158
|
-
relative_target = _unused_name(resource_dir, basename, used[resource_kind])
|
|
159
|
-
else:
|
|
160
|
-
relative_target = _safe_resource_target(target_path)
|
|
88
|
+
relative_target = _unused_name(resource_dir, os.path.basename(source), used[resource_kind])
|
|
161
89
|
copied_path = os.path.join(resource_dir, relative_target)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
raise ValueError(f"JianYing resource target escapes package: {target_path}")
|
|
165
|
-
os.makedirs(os.path.dirname(copied_path), exist_ok=True)
|
|
166
|
-
if os.path.exists(copied_path):
|
|
167
|
-
raise FileExistsError(f"duplicate JianYing resource target: {relative_target}")
|
|
168
|
-
if is_zip:
|
|
169
|
-
_extract_zip(source, copied_path)
|
|
170
|
-
elif os.path.isdir(source):
|
|
171
|
-
shutil.copytree(source, copied_path)
|
|
172
|
-
else:
|
|
173
|
-
shutil.copy2(source, copied_path)
|
|
174
|
-
relative_path = f"Resources/local/{resource_kind}/{relative_target.replace(os.sep, '/')}"
|
|
90
|
+
shutil.copy2(source, copied_path)
|
|
91
|
+
relative_path = f"Resources/local/{resource_kind}/{relative_target}"
|
|
175
92
|
return copied_path, relative_path, f"{DRAFT_PATH_PLACEHOLDER}/{relative_path}"
|
|
176
93
|
|
|
177
94
|
|
|
@@ -179,41 +96,6 @@ def _is_packaged_path(value):
|
|
|
179
96
|
return str(value).startswith((DRAFT_PATH_PLACEHOLDER, "Resources/", "./Resources/"))
|
|
180
97
|
|
|
181
98
|
|
|
182
|
-
def _descriptor(raw, default_kind, *, required):
|
|
183
|
-
"""`raw` is a contract-validated resource entry: an object with a non-empty source_path."""
|
|
184
|
-
source = raw["source_path"]
|
|
185
|
-
resource_kind = raw.get("resource_kind", default_kind)
|
|
186
|
-
target_path = raw.get("target_path")
|
|
187
|
-
if not isinstance(resource_kind, str) or resource_kind not in {
|
|
188
|
-
"audio", "effect", "fonts", "image", "lut", "mask", "sticker",
|
|
189
|
-
"text", "text_template", "transition", "video",
|
|
190
|
-
}:
|
|
191
|
-
raise ValueError(f"invalid JianYing resource kind: {resource_kind}")
|
|
192
|
-
return {
|
|
193
|
-
"source_path": source,
|
|
194
|
-
"resource_kind": resource_kind,
|
|
195
|
-
"target_path": target_path,
|
|
196
|
-
"required": required,
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _material_resource_descriptors(material, default_kind):
|
|
201
|
-
descriptors = [
|
|
202
|
-
_descriptor(raw, default_kind, required=True)
|
|
203
|
-
for raw in material.get("_bundle_resources", [])
|
|
204
|
-
]
|
|
205
|
-
path = material.get("path")
|
|
206
|
-
if isinstance(path, str) and path and not _is_packaged_path(path):
|
|
207
|
-
if not any(item["source_path"] == path for item in descriptors):
|
|
208
|
-
descriptors.append({
|
|
209
|
-
"source_path": path,
|
|
210
|
-
"resource_kind": default_kind,
|
|
211
|
-
"target_path": None,
|
|
212
|
-
"required": False,
|
|
213
|
-
})
|
|
214
|
-
return descriptors
|
|
215
|
-
|
|
216
|
-
|
|
217
99
|
def bundle_media(content, meta, draft_dir):
|
|
218
100
|
"""Copy media into duo-video's Resources/local contract and index it.
|
|
219
101
|
|
|
@@ -224,83 +106,34 @@ def bundle_media(content, meta, draft_dir):
|
|
|
224
106
|
"""
|
|
225
107
|
resources_root = os.path.join(draft_dir, "Resources", "local")
|
|
226
108
|
copied = {}
|
|
227
|
-
|
|
228
|
-
"audio", "effect", "fonts", "image", "lut", "mask", "sticker",
|
|
229
|
-
"text", "text_template", "transition", "video",
|
|
230
|
-
}
|
|
231
|
-
used = {kind: set() for kind in resource_kinds}
|
|
109
|
+
used = {kind: set() for kind in ("audio", "image", "video")}
|
|
232
110
|
meta_values = []
|
|
233
111
|
notes = []
|
|
234
112
|
timestamp_ms = int(time.time() * 1000)
|
|
235
113
|
|
|
236
|
-
|
|
237
|
-
for
|
|
238
|
-
for
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
material
|
|
258
|
-
|
|
259
|
-
meta_values.append(
|
|
260
|
-
_meta_value(material, relative_path, metetype, copied_path, timestamp_ms)
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
for materials_key, resource_kind in RESOURCE_DIRECTORY_BY_MATERIALS_KEY.items():
|
|
264
|
-
for material in materials.get(materials_key, []):
|
|
265
|
-
descriptors = _material_resource_descriptors(
|
|
266
|
-
material, resource_kind
|
|
267
|
-
)
|
|
268
|
-
seen_descriptors = set()
|
|
269
|
-
for descriptor in descriptors:
|
|
270
|
-
src = descriptor["source_path"]
|
|
271
|
-
if _is_packaged_path(src):
|
|
272
|
-
continue
|
|
273
|
-
descriptor_key = (
|
|
274
|
-
descriptor["resource_kind"],
|
|
275
|
-
os.path.realpath(src),
|
|
276
|
-
descriptor["target_path"],
|
|
277
|
-
)
|
|
278
|
-
if descriptor_key in seen_descriptors:
|
|
279
|
-
continue
|
|
280
|
-
seen_descriptors.add(descriptor_key)
|
|
281
|
-
if not os.path.exists(src):
|
|
282
|
-
message = f"声明的剪映资源缺失: {src}"
|
|
283
|
-
if descriptor["required"]:
|
|
284
|
-
raise ValueError(message)
|
|
285
|
-
notes.append(message)
|
|
286
|
-
continue
|
|
287
|
-
kind = descriptor["resource_kind"]
|
|
288
|
-
source_key = (kind, os.path.realpath(src), descriptor["target_path"])
|
|
289
|
-
existing = copied.get(source_key)
|
|
290
|
-
if existing is None:
|
|
291
|
-
_copied_path, _relative_path, draft_path = _copy_resource(
|
|
292
|
-
src,
|
|
293
|
-
kind,
|
|
294
|
-
resources_root,
|
|
295
|
-
used,
|
|
296
|
-
target_path=descriptor["target_path"],
|
|
297
|
-
)
|
|
298
|
-
copied[source_key] = {"draft_path": draft_path}
|
|
299
|
-
else:
|
|
300
|
-
draft_path = existing["draft_path"]
|
|
301
|
-
_replace_material_resource_path(
|
|
302
|
-
material, materials_key, src, draft_path
|
|
303
|
-
)
|
|
114
|
+
materials = content["materials"]
|
|
115
|
+
for materials_key in ("videos", "audios"):
|
|
116
|
+
for material in materials[materials_key]:
|
|
117
|
+
src = material["path"]
|
|
118
|
+
resource_kind, metetype = _resource_kind(material, materials_key)
|
|
119
|
+
source_key = (resource_kind, os.path.realpath(src))
|
|
120
|
+
existing = copied.get(source_key)
|
|
121
|
+
if existing is not None:
|
|
122
|
+
material["path"] = existing
|
|
123
|
+
continue
|
|
124
|
+
if not os.path.isfile(src):
|
|
125
|
+
if not _is_packaged_path(src):
|
|
126
|
+
notes.append(f"素材缺失,未打包: {src}")
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
copied_path, relative_path, draft_path = _copy_resource(
|
|
130
|
+
src, resource_kind, resources_root, used
|
|
131
|
+
)
|
|
132
|
+
material["path"] = draft_path
|
|
133
|
+
copied[source_key] = draft_path
|
|
134
|
+
meta_values.append(
|
|
135
|
+
_meta_value(material, relative_path, metetype, copied_path, timestamp_ms)
|
|
136
|
+
)
|
|
304
137
|
|
|
305
138
|
material_group = next(group for group in meta["draft_materials"] if group["type"] == 0)
|
|
306
139
|
material_group["value"] = meta_values
|
|
@@ -311,13 +144,6 @@ def bundle_media(content, meta, draft_dir):
|
|
|
311
144
|
return notes
|
|
312
145
|
|
|
313
146
|
|
|
314
|
-
def strip_internal_resource_fields(content):
|
|
315
|
-
for materials in _material_sets(content):
|
|
316
|
-
for entries in materials.values():
|
|
317
|
-
for material in entries:
|
|
318
|
-
material.pop("_bundle_resources", None)
|
|
319
|
-
|
|
320
|
-
|
|
321
147
|
def draft_dir_has_user_content(draft_dir):
|
|
322
148
|
"""Return True when writing here could overwrite an existing draft/material."""
|
|
323
149
|
if not os.path.exists(draft_dir):
|
|
@@ -359,7 +185,6 @@ def write_draft(content, meta, notes, out_dir, draft_name, bundle_media_enabled=
|
|
|
359
185
|
os.makedirs(tmp_dir, exist_ok=False)
|
|
360
186
|
if bundle_media_enabled:
|
|
361
187
|
notes.extend(bundle_media(content, meta, tmp_dir))
|
|
362
|
-
strip_internal_resource_fields(content)
|
|
363
188
|
timestamp_ms = int(time.time() * 1000)
|
|
364
189
|
meta["draft_name"] = actual_name
|
|
365
190
|
meta["draft_fold_path"] = draft_dir
|