From 7a846d93c6eaa319aefa3f9e055837e38f8b4526 Mon Sep 17 00:00:00 2001 From: KeyInfo Bot Date: Wed, 12 Aug 2026 00:02:07 +0800 Subject: [PATCH] Sync third-party and MCP marketplace plugins Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources. Confidence: high Scope-risk: narrow Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history. Tested: Marketplace validation passed. --- config/external-sources.lock.json | 16 +- .../plugins/caveman/THIRD_PARTY_SOURCE.json | 4 +- .../plugins/mcp-playwright/MCP_SOURCE.json | 2 +- .../next-skills/THIRD_PARTY_SOURCE.json | 4 +- .../next-cache-components-optimizer/SKILL.md | 10 +- .../reference/patterns.md | 10 +- .../SKILL.md | 22 +- plugins/codex/plugins/ppt-master/README.md | 10 +- .../ppt-master/THIRD_PARTY_SOURCE.json | 4 +- .../ppt-master/skills/ppt-master/SPONSORS.md | 4 +- .../skills/ppt-master/SPONSORS_CN.md | 4 +- .../ppt-master/references/animations.md | 82 +- .../ppt-master/references/executor-base.md | 2 +- .../ppt-master/references/executor-image.md | 4 +- .../references/executor-web-image.md | 6 +- .../ppt-master/references/image-base.md | 56 +- .../references/image-layout-patterns.md | 2 +- .../ppt-master/references/strategist-image.md | 8 +- .../ppt-master/references/strategist.md | 6 +- .../references/svg-image-embedding.md | 7 +- .../ppt-master/references/video-design.md | 108 +- .../skills/ppt-master/scripts/README.md | 3 +- .../skills/ppt-master/scripts/docs/image.md | 33 + .../scripts/docs/pptx-animations.md | 11 +- .../scripts/docs/pptx-transitions.md | 33 +- .../ppt-master/scripts/docs/svg-pipeline.md | 5 +- .../skills/ppt-master/scripts/image_treat.py | 526 ++ .../ppt-master/scripts/narration_sync.py | 222 +- .../ppt-master/scripts/pptx_transitions.py | 383 +- .../scripts/prompt_audit_manifest.json | 52 +- .../skills/ppt-master/scripts/sound_sync.py | 289 ++ .../scripts/svg_to_pptx/animation_config.py | 111 +- .../svg_to_pptx/pptx_package/builder.py | 224 +- .../scripts/svg_to_pptx/pptx_package/cli.py | 34 +- .../ppt-master/scripts/video_sound_mix.py | 1624 ++++++ .../ppt-master/scripts/video_subtitles.py | 13 +- .../skills/ppt-master/templates/README.md | 5 + .../ppt-master/templates/sounds/CC0-1.0.txt | 121 + .../ppt-master/templates/sounds/README.md | 61 + .../templates/sounds/THIRD_PARTY_NOTICES.md | 71 + .../templates/sounds/bigsoundbank/0572.wav | Bin 0 -> 958776 bytes .../templates/sounds/bigsoundbank/0573.wav | Bin 0 -> 1084718 bytes .../templates/sounds/bigsoundbank/1795.wav | Bin 0 -> 161238 bytes .../templates/sounds/bigsoundbank/1796.wav | Bin 0 -> 182862 bytes .../templates/sounds/bigsoundbank/1797.wav | Bin 0 -> 94670 bytes .../templates/sounds/bigsoundbank/1798.wav | Bin 0 -> 81310 bytes .../templates/sounds/bigsoundbank/1799.wav | Bin 0 -> 91210 bytes .../templates/sounds/bigsoundbank/1800.wav | Bin 0 -> 156498 bytes .../templates/sounds/bigsoundbank/1801.wav | Bin 0 -> 142118 bytes .../templates/sounds/bigsoundbank/1802.wav | Bin 0 -> 65846 bytes .../templates/sounds/bigsoundbank/2059.wav | Bin 0 -> 26540 bytes .../templates/sounds/bigsoundbank/2060.wav | Bin 0 -> 26538 bytes .../templates/sounds/bigsoundbank/2061.wav | Bin 0 -> 26540 bytes .../templates/sounds/bigsoundbank/2062.wav | Bin 0 -> 35358 bytes .../templates/sounds/bigsoundbank/2063.wav | Bin 0 -> 35360 bytes .../templates/sounds/bigsoundbank/2064.wav | Bin 0 -> 35360 bytes .../templates/sounds/bigsoundbank/2065.wav | Bin 0 -> 35358 bytes .../templates/sounds/bigsoundbank/2066.wav | Bin 0 -> 35358 bytes .../templates/sounds/bigsoundbank/2067.wav | Bin 0 -> 35358 bytes .../templates/sounds/bigsoundbank/2079.wav | Bin 0 -> 202104 bytes .../templates/sounds/bigsoundbank/2080.wav | Bin 0 -> 127196 bytes .../templates/sounds/bigsoundbank/2081.wav | Bin 0 -> 101024 bytes .../templates/sounds/bigsoundbank/2082.wav | Bin 0 -> 164576 bytes .../templates/sounds/bigsoundbank/2083.wav | Bin 0 -> 138412 bytes .../templates/sounds/bigsoundbank/2084.wav | Bin 0 -> 153360 bytes .../templates/sounds/bigsoundbank/2085.wav | Bin 0 -> 157082 bytes .../templates/sounds/bigsoundbank/2086.wav | Bin 0 -> 228136 bytes .../templates/sounds/bigsoundbank/2087.wav | Bin 0 -> 183262 bytes .../templates/sounds/bigsoundbank/2088.wav | Bin 0 -> 201966 bytes .../templates/sounds/bigsoundbank/2089.wav | Bin 0 -> 198228 bytes .../templates/sounds/bigsoundbank/2090.wav | Bin 0 -> 523490 bytes .../templates/sounds/bigsoundbank/2091.wav | Bin 0 -> 467178 bytes .../templates/sounds/bigsoundbank/3236.wav | Bin 0 -> 297514 bytes .../templates/sounds/bigsoundbank/3237.wav | Bin 0 -> 155564 bytes .../templates/sounds/bigsoundbank/3238.wav | Bin 0 -> 189366 bytes .../sounds/kenney-interface/back_001.wav | Bin 0 -> 5456 bytes .../sounds/kenney-interface/back_002.wav | Bin 0 -> 8142 bytes .../sounds/kenney-interface/back_003.wav | Bin 0 -> 8014 bytes .../sounds/kenney-interface/back_004.wav | Bin 0 -> 8398 bytes .../sounds/kenney-interface/bong_001.wav | Bin 0 -> 10656 bytes .../sounds/kenney-interface/click_001.wav | Bin 0 -> 8644 bytes .../sounds/kenney-interface/click_002.wav | Bin 0 -> 1102 bytes .../sounds/kenney-interface/click_003.wav | Bin 0 -> 706 bytes .../sounds/kenney-interface/click_004.wav | Bin 0 -> 1998 bytes .../sounds/kenney-interface/click_005.wav | Bin 0 -> 706 bytes .../sounds/kenney-interface/close_001.wav | Bin 0 -> 13112 bytes .../sounds/kenney-interface/close_002.wav | Bin 0 -> 27754 bytes .../sounds/kenney-interface/close_003.wav | Bin 0 -> 27754 bytes .../sounds/kenney-interface/close_004.wav | Bin 0 -> 28556 bytes .../kenney-interface/confirmation_001.wav | Bin 0 -> 25642 bytes .../kenney-interface/confirmation_002.wav | Bin 0 -> 47618 bytes .../kenney-interface/confirmation_003.wav | Bin 0 -> 28480 bytes .../kenney-interface/confirmation_004.wav | Bin 0 -> 43332 bytes .../sounds/kenney-interface/drop_001.wav | Bin 0 -> 23374 bytes .../sounds/kenney-interface/drop_002.wav | Bin 0 -> 33274 bytes .../sounds/kenney-interface/drop_003.wav | Bin 0 -> 33274 bytes .../sounds/kenney-interface/drop_004.wav | Bin 0 -> 50138 bytes .../sounds/kenney-interface/error_001.wav | Bin 0 -> 28594 bytes .../sounds/kenney-interface/error_002.wav | Bin 0 -> 29262 bytes .../sounds/kenney-interface/error_003.wav | Bin 0 -> 94182 bytes .../sounds/kenney-interface/error_004.wav | Bin 0 -> 18510 bytes .../sounds/kenney-interface/error_005.wav | Bin 0 -> 43926 bytes .../sounds/kenney-interface/error_006.wav | Bin 0 -> 44182 bytes .../sounds/kenney-interface/error_007.wav | Bin 0 -> 17012 bytes .../sounds/kenney-interface/error_008.wav | Bin 0 -> 12366 bytes .../sounds/kenney-interface/glass_001.wav | Bin 0 -> 24380 bytes .../sounds/kenney-interface/glass_002.wav | Bin 0 -> 10874 bytes .../sounds/kenney-interface/glass_003.wav | Bin 0 -> 10730 bytes .../sounds/kenney-interface/glass_004.wav | Bin 0 -> 61136 bytes .../sounds/kenney-interface/glass_005.wav | Bin 0 -> 11726 bytes .../sounds/kenney-interface/glass_006.wav | Bin 0 -> 11726 bytes .../sounds/kenney-interface/glitch_001.wav | Bin 0 -> 3098 bytes .../sounds/kenney-interface/glitch_002.wav | Bin 0 -> 4862 bytes .../sounds/kenney-interface/glitch_003.wav | Bin 0 -> 2126 bytes .../sounds/kenney-interface/glitch_004.wav | Bin 0 -> 3606 bytes .../sounds/kenney-interface/maximize_001.wav | Bin 0 -> 22868 bytes .../sounds/kenney-interface/maximize_002.wav | Bin 0 -> 22858 bytes .../sounds/kenney-interface/maximize_003.wav | Bin 0 -> 18764 bytes .../sounds/kenney-interface/maximize_004.wav | Bin 0 -> 36942 bytes .../sounds/kenney-interface/maximize_005.wav | Bin 0 -> 46504 bytes .../sounds/kenney-interface/maximize_006.wav | Bin 0 -> 33590 bytes .../sounds/kenney-interface/maximize_007.wav | Bin 0 -> 16206 bytes .../sounds/kenney-interface/maximize_008.wav | Bin 0 -> 19954 bytes .../sounds/kenney-interface/maximize_009.wav | Bin 0 -> 19648 bytes .../sounds/kenney-interface/minimize_001.wav | Bin 0 -> 22868 bytes .../sounds/kenney-interface/minimize_002.wav | Bin 0 -> 22858 bytes .../sounds/kenney-interface/minimize_003.wav | Bin 0 -> 18764 bytes .../sounds/kenney-interface/minimize_004.wav | Bin 0 -> 36942 bytes .../sounds/kenney-interface/minimize_005.wav | Bin 0 -> 46504 bytes .../sounds/kenney-interface/minimize_006.wav | Bin 0 -> 33590 bytes .../sounds/kenney-interface/minimize_007.wav | Bin 0 -> 16206 bytes .../sounds/kenney-interface/minimize_008.wav | Bin 0 -> 19954 bytes .../sounds/kenney-interface/minimize_009.wav | Bin 0 -> 19648 bytes .../sounds/kenney-interface/open_001.wav | Bin 0 -> 13112 bytes .../sounds/kenney-interface/open_002.wav | Bin 0 -> 27754 bytes .../sounds/kenney-interface/open_003.wav | Bin 0 -> 27754 bytes .../sounds/kenney-interface/open_004.wav | Bin 0 -> 28556 bytes .../sounds/kenney-interface/pluck_001.wav | Bin 0 -> 17634 bytes .../sounds/kenney-interface/pluck_002.wav | Bin 0 -> 28594 bytes .../sounds/kenney-interface/question_001.wav | Bin 0 -> 43356 bytes .../sounds/kenney-interface/question_002.wav | Bin 0 -> 29406 bytes .../sounds/kenney-interface/question_003.wav | Bin 0 -> 29148 bytes .../sounds/kenney-interface/question_004.wav | Bin 0 -> 29402 bytes .../sounds/kenney-interface/scratch_001.wav | Bin 0 -> 12366 bytes .../sounds/kenney-interface/scratch_002.wav | Bin 0 -> 12366 bytes .../sounds/kenney-interface/scratch_003.wav | Bin 0 -> 10964 bytes .../sounds/kenney-interface/scratch_004.wav | Bin 0 -> 28750 bytes .../sounds/kenney-interface/scratch_005.wav | Bin 0 -> 28750 bytes .../sounds/kenney-interface/scroll_001.wav | Bin 0 -> 88280 bytes .../sounds/kenney-interface/scroll_002.wav | Bin 0 -> 88280 bytes .../sounds/kenney-interface/scroll_003.wav | Bin 0 -> 88280 bytes .../sounds/kenney-interface/scroll_004.wav | Bin 0 -> 88280 bytes .../sounds/kenney-interface/scroll_005.wav | Bin 0 -> 88280 bytes .../sounds/kenney-interface/select_001.wav | Bin 0 -> 7202 bytes .../sounds/kenney-interface/select_002.wav | Bin 0 -> 7202 bytes .../sounds/kenney-interface/select_003.wav | Bin 0 -> 33582 bytes .../sounds/kenney-interface/select_004.wav | Bin 0 -> 33582 bytes .../sounds/kenney-interface/select_005.wav | Bin 0 -> 33582 bytes .../sounds/kenney-interface/select_006.wav | Bin 0 -> 171276 bytes .../sounds/kenney-interface/select_007.wav | Bin 0 -> 5326 bytes .../sounds/kenney-interface/select_008.wav | Bin 0 -> 5326 bytes .../sounds/kenney-interface/switch_001.wav | Bin 0 -> 108522 bytes .../sounds/kenney-interface/switch_002.wav | Bin 0 -> 107338 bytes .../sounds/kenney-interface/switch_003.wav | Bin 0 -> 88278 bytes .../sounds/kenney-interface/switch_004.wav | Bin 0 -> 87766 bytes .../sounds/kenney-interface/switch_005.wav | Bin 0 -> 107510 bytes .../sounds/kenney-interface/switch_006.wav | Bin 0 -> 107266 bytes .../sounds/kenney-interface/switch_007.wav | Bin 0 -> 107938 bytes .../sounds/kenney-interface/tick_001.wav | Bin 0 -> 4046 bytes .../sounds/kenney-interface/tick_002.wav | Bin 0 -> 1870 bytes .../sounds/kenney-interface/tick_004.wav | Bin 0 -> 4942 bytes .../sounds/kenney-interface/toggle_001.wav | Bin 0 -> 12110 bytes .../sounds/kenney-interface/toggle_002.wav | Bin 0 -> 12110 bytes .../sounds/kenney-interface/toggle_003.wav | Bin 0 -> 12366 bytes .../sounds/kenney-interface/toggle_004.wav | Bin 0 -> 5658 bytes .../templates/sounds/kenney-ui/click1.wav | Bin 0 -> 16118 bytes .../templates/sounds/kenney-ui/click2.wav | Bin 0 -> 9398 bytes .../templates/sounds/kenney-ui/click3.wav | Bin 0 -> 14762 bytes .../templates/sounds/kenney-ui/click4.wav | Bin 0 -> 6734 bytes .../templates/sounds/kenney-ui/click5.wav | Bin 0 -> 6222 bytes .../sounds/kenney-ui/mouseclick1.wav | Bin 0 -> 10038 bytes .../sounds/kenney-ui/mouserelease1.wav | Bin 0 -> 11746 bytes .../templates/sounds/kenney-ui/rollover1.wav | Bin 0 -> 39658 bytes .../templates/sounds/kenney-ui/rollover2.wav | Bin 0 -> 9678 bytes .../templates/sounds/kenney-ui/rollover3.wav | Bin 0 -> 11790 bytes .../templates/sounds/kenney-ui/rollover4.wav | Bin 0 -> 19534 bytes .../templates/sounds/kenney-ui/rollover5.wav | Bin 0 -> 19278 bytes .../templates/sounds/kenney-ui/rollover6.wav | Bin 0 -> 30346 bytes .../templates/sounds/kenney-ui/switch1.wav | Bin 0 -> 55078 bytes .../templates/sounds/kenney-ui/switch10.wav | Bin 0 -> 68430 bytes .../templates/sounds/kenney-ui/switch11.wav | Bin 0 -> 51994 bytes .../templates/sounds/kenney-ui/switch12.wav | Bin 0 -> 9014 bytes .../templates/sounds/kenney-ui/switch13.wav | Bin 0 -> 4438 bytes .../templates/sounds/kenney-ui/switch14.wav | Bin 0 -> 6222 bytes .../templates/sounds/kenney-ui/switch15.wav | Bin 0 -> 44738 bytes .../templates/sounds/kenney-ui/switch16.wav | Bin 0 -> 62806 bytes .../templates/sounds/kenney-ui/switch17.wav | Bin 0 -> 58290 bytes .../templates/sounds/kenney-ui/switch18.wav | Bin 0 -> 76358 bytes .../templates/sounds/kenney-ui/switch19.wav | Bin 0 -> 67326 bytes .../templates/sounds/kenney-ui/switch2.wav | Bin 0 -> 56142 bytes .../templates/sounds/kenney-ui/switch20.wav | Bin 0 -> 62810 bytes .../templates/sounds/kenney-ui/switch21.wav | Bin 0 -> 71842 bytes .../templates/sounds/kenney-ui/switch22.wav | Bin 0 -> 62806 bytes .../templates/sounds/kenney-ui/switch23.wav | Bin 0 -> 67322 bytes .../templates/sounds/kenney-ui/switch24.wav | Bin 0 -> 49254 bytes .../templates/sounds/kenney-ui/switch25.wav | Bin 0 -> 67326 bytes .../templates/sounds/kenney-ui/switch26.wav | Bin 0 -> 40222 bytes .../templates/sounds/kenney-ui/switch27.wav | Bin 0 -> 49254 bytes .../templates/sounds/kenney-ui/switch28.wav | Bin 0 -> 35706 bytes .../templates/sounds/kenney-ui/switch29.wav | Bin 0 -> 53770 bytes .../templates/sounds/kenney-ui/switch3.wav | Bin 0 -> 64330 bytes .../templates/sounds/kenney-ui/switch30.wav | Bin 0 -> 62810 bytes .../templates/sounds/kenney-ui/switch31.wav | Bin 0 -> 76358 bytes .../templates/sounds/kenney-ui/switch32.wav | Bin 0 -> 76358 bytes .../templates/sounds/kenney-ui/switch33.wav | Bin 0 -> 90422 bytes .../templates/sounds/kenney-ui/switch34.wav | Bin 0 -> 85326 bytes .../templates/sounds/kenney-ui/switch35.wav | Bin 0 -> 58290 bytes .../templates/sounds/kenney-ui/switch36.wav | Bin 0 -> 58190 bytes .../templates/sounds/kenney-ui/switch37.wav | Bin 0 -> 53774 bytes .../templates/sounds/kenney-ui/switch38.wav | Bin 0 -> 74326 bytes .../templates/sounds/kenney-ui/switch4.wav | Bin 0 -> 73582 bytes .../templates/sounds/kenney-ui/switch5.wav | Bin 0 -> 55078 bytes .../templates/sounds/kenney-ui/switch6.wav | Bin 0 -> 61246 bytes .../templates/sounds/kenney-ui/switch7.wav | Bin 0 -> 37710 bytes .../templates/sounds/kenney-ui/switch8.wav | Bin 0 -> 58162 bytes .../templates/sounds/kenney-ui/switch9.wav | Bin 0 -> 45826 bytes .../templates/sounds/sounds_index.json | 4536 +++++++++++++++++ .../ppt-master/workflows/generate-pptx.md | 39 +- .../workflows/profiles/quick-generate.md | 115 +- .../workflows/stages/customize-animations.md | 50 +- .../workflows/stages/generate-audio.md | 119 +- .../plugins/shadcn/THIRD_PARTY_SOURCE.json | 4 +- 232 files changed, 8709 insertions(+), 346 deletions(-) create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_treat.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/sound_sync.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_sound_mix.py create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/CC0-1.0.txt create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/README.md create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/THIRD_PARTY_NOTICES.md create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/0572.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/0573.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1795.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1796.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1797.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1798.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1799.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1800.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1801.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/1802.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2059.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2060.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2061.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2062.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2063.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2064.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2065.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2066.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2067.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2079.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2080.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2081.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2082.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2083.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2084.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2085.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2086.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2087.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2088.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2089.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2090.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/2091.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/3236.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/3237.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/3238.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/back_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/back_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/back_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/back_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/bong_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/click_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/click_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/click_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/click_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/click_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/close_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/close_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/close_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/close_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/confirmation_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/confirmation_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/confirmation_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/confirmation_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/drop_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/drop_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/drop_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/drop_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_006.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_007.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/error_008.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glass_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glass_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glass_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glass_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glass_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glass_006.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glitch_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glitch_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glitch_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/glitch_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_006.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_007.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_008.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/maximize_009.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_006.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_007.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_008.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/minimize_009.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/open_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/open_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/open_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/open_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/pluck_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/pluck_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/question_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/question_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/question_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/question_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scratch_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scratch_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scratch_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scratch_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scratch_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scroll_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scroll_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scroll_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scroll_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/scroll_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_006.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_007.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/select_008.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_005.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_006.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/switch_007.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/tick_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/tick_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/tick_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/toggle_001.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/toggle_002.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/toggle_003.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-interface/toggle_004.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/click1.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/click2.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/click3.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/click4.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/click5.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/mouseclick1.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/mouserelease1.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/rollover1.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/rollover2.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/rollover3.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/rollover4.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/rollover5.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/rollover6.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch1.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch10.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch11.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch12.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch13.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch14.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch15.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch16.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch17.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch18.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch19.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch2.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch20.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch21.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch22.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch23.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch24.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch25.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch26.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch27.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch28.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch29.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch3.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch30.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch31.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch32.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch33.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch34.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch35.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch36.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch37.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch38.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch4.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch5.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch6.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch7.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch8.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/kenney-ui/switch9.wav create mode 100644 plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/sounds_index.json diff --git a/config/external-sources.lock.json b/config/external-sources.lock.json index 276fabcd..124327f7 100644 --- a/config/external-sources.lock.json +++ b/config/external-sources.lock.json @@ -42,8 +42,8 @@ "repo": "https://github.com/JuliusBrussee/caveman.git", "ref": "main", "adapter": "codex-plugin", - "commit": "11ddc0c9813c8f75365cd5be2f753df08712f154", - "syncedAt": "2026-08-08T16:00:01Z" + "commit": "b2ca50b8809d4dbc9f664b398938fddede320001", + "syncedAt": "2026-08-11T16:00:01Z" }, { "id": "taste-skill", @@ -60,8 +60,8 @@ "repo": "https://github.com/shadcn-ui/ui.git", "ref": "main", "adapter": "claude-skill", - "commit": "deda4df80fb350230b2fce2b575e769a90cae076", - "syncedAt": "2026-08-10T15:59:59Z" + "commit": "41bbc12cfd39ed8d9cb8da04275479ee7ecc0612", + "syncedAt": "2026-08-11T16:00:01Z" }, { "id": "frontend-slides", @@ -96,8 +96,8 @@ "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", "adapter": "claude-skill", - "commit": "182c6b8229a44990cdc5b394545f90992be377d6", - "syncedAt": "2026-08-10T15:59:59Z" + "commit": "4e6ecbcb0dc079efebd3c79b775c0f02581509fe", + "syncedAt": "2026-08-11T16:00:01Z" }, { "id": "next-skills", @@ -105,8 +105,8 @@ "repo": "https://github.com/vercel/next.js.git", "ref": "canary", "adapter": "skill-collection", - "commit": "1722e45c3957d1e6ce2aaff482a9b9ece28864a1", - "syncedAt": "2026-08-10T15:59:59Z" + "commit": "bbec4fe2bd7222aaa9b4de58269d821ab128fab8", + "syncedAt": "2026-08-11T16:00:01Z" } ] } diff --git a/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json index ae8c3bde..d8280abd 100644 --- a/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/caveman/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "caveman", "repo": "https://github.com/JuliusBrussee/caveman.git", "ref": "main", - "commit": "11ddc0c9813c8f75365cd5be2f753df08712f154", + "commit": "b2ca50b8809d4dbc9f664b398938fddede320001", "adapter": "codex-plugin", "sourcePath": "plugins/caveman", - "syncedAt": "2026-08-08T16:00:01Z" + "syncedAt": "2026-08-11T16:00:01Z" } diff --git a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json index c22a3fd1..f373bf2e 100644 --- a/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json +++ b/plugins/codex/plugins/mcp-playwright/MCP_SOURCE.json @@ -3,5 +3,5 @@ "name": "playwright浏览器自动化操作", "version": "20260605", "keySource": "none", - "syncedAt": "2026-08-10T16:02:03Z" + "syncedAt": "2026-08-11T16:02:06Z" } diff --git a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json index b0a21531..b066ff31 100644 --- a/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/next-skills/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "next-skills", "repo": "https://github.com/vercel/next.js.git", "ref": "canary", - "commit": "1722e45c3957d1e6ce2aaff482a9b9ece28864a1", + "commit": "bbec4fe2bd7222aaa9b4de58269d821ab128fab8", "adapter": "skill-collection", "sourcePath": "skills", - "syncedAt": "2026-08-10T15:59:59Z" + "syncedAt": "2026-08-11T16:00:01Z" } diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md index a195dc27..415b3ba9 100644 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/SKILL.md @@ -96,8 +96,8 @@ hear those words. transcript of the loop. - **Only surface a question for a genuine fork:** a fix that would change behavior, a security-sensitive read, or a route that's dynamic by design (a - runtime-prefetch candidate, not a shell to grow). A clean instant fix is not a - fork — keep going. With no one to ask (an unattended run), don't block: take + per-link-prefetch candidate, not a shell to grow). A clean instant fix is not + a fork — keep going. With no one to ask (an unattended run), don't block: take the safe default and note the assumption — for a cache-freshness choice, defer the read behind `` (always fresh, still instant) rather than guess a `cacheLife`. @@ -359,10 +359,10 @@ this gate is as machine-checkable as the others. Detail: **When URL data can't be pushed down** (for example, the whole page depends on `params`, `searchParams`, or the full URL), there may be no meaningful static -shell to grow. Don't force one. Runtime prefetching can make the soft +shell to grow. Don't force one. Per-link prefetching can make the soft navigation instant, but it is outside this optimizer loop: it requires Partial Prefetching, a ``, and cached URL-dependent content. See -[Runtime Prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) +[Optimizing prefetching](https://nextjs.org/docs/app/guides/optimizing-prefetching) and pattern 10 in `reference/patterns.md` for the requirements, cost trade-offs, manual prefetch caveat, and `instant()` test gotchas. @@ -463,5 +463,5 @@ during an incremental rollout and keep checking any other target routes. That skill moves the app onto the better prefetching model: shared App Shell prefetches by default, fewer duplicated full-prefetch requests for visible links, a link audit for existing `` usage, and optional - per-link runtime prefetching only where URL-specific content is worth the + per-link prefetching only where URL-specific content is worth the extra server work. diff --git a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md index 6891b647..9ec0cc04 100644 --- a/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md +++ b/plugins/codex/plugins/next-skills/skills/next-cache-components-optimizer/reference/patterns.md @@ -323,9 +323,9 @@ Prefer per-component boundaries inside the page (patterns #1–#5) over one big Patterns 1–9 grow a **static shell** by moving dynamic reads behind boundaries. Session data from `cookies()` and `headers()` is handled by the earlier patterns. URL data is different: `params`, `searchParams`, and the full URL belong to one link, while the App Shell is shared by every link to the route. -If the whole route depends on URL data, pushing the read lower may leave no meaningful shared shell to commit. That is the optimizer's stop point, not another shell refactor. Return to `SKILL.md` after the optimization loop for the optional runtime-prefetch follow-up. +If the whole route depends on URL data, pushing the read lower may leave no meaningful shared shell to commit. That is the optimizer's stop point, not another shell refactor. Return to `SKILL.md` after the optimization loop for the optional per-link-prefetch follow-up. -Runtime prefetching is the only way for this soft navigation to commit the +Per-link prefetching is the only way for this soft navigation to commit the URL-specific content before the click. It has **three requirements**: ```tsx @@ -347,11 +347,11 @@ Under `instant()` the runtime entry is what commits, so the real content, not a Gotchas (each cost real debugging time): - **The full prefetch is mandatory.** With App Shells enabled an auto/PPR prefetch bails before the runtime spawn (`subtreeHasSpeculativePrefetch`); use `` for normal links, or keep an existing manual full-prefetch abstraction if the app already owns one. If the route is still RED after caching the URL-dependent content, the navigation may still be doing an auto prefetch. -- **Partial Prefetching must be adopted for the destination.** Runtime prefetching rides on the Partial Prefetching path. If the route is still RED after caching the URL-dependent content, check whether the link is still doing an auto prefetch or whether the destination never adopted Partial Prefetching. +- **Partial Prefetching must be adopted for the destination.** Per-link prefetching uses the Partial Prefetching path. If the route is still RED after caching the URL-dependent content, check whether the link is still doing an auto prefetch or whether the destination never adopted Partial Prefetching. - **Prefetch the canonical URL.** A link whose href 307-redirects (a `/foo` that canonicalizes to `/`) can't be prefetched — the prefetch receives the redirect, not the tree. Point the link and the prefetch at the final URL. -- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; enabling it for every visible link is wasteful. Scope `prefetch={true}` to the runtime-prefetch targets only, using the [runtime prefetching trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) and [hover-triggered prefetch](https://nextjs.org/docs/app/guides/prefetching#hover-triggered-prefetch) when many links are visible. +- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; enabling it for every visible link is wasteful. Scope `prefetch={true}` to the per-link-prefetch targets only, using the [trade-offs](https://nextjs.org/docs/app/guides/optimizing-prefetching#trade-offs) and [hover-triggered prefetch](https://nextjs.org/docs/app/guides/prefetching#hover-triggered-prefetch) when many links are visible. - **Marker must be a committed node, not RSC bytes.** The content is often a client component, so its text isn't in the prefetch response — assert a `data-testid` that renders when the client subtree commits, not a substring of the stream. -Prefer a static shell (patterns 1–9) whenever the URL-data read can move: it's cheaper than a runtime prefetch and also covers hard load. Runtime prefetching is only for URL-data reads that genuinely can't move, or routes whose useful content is all URL-specific. +Prefer a static shell (patterns 1–9) whenever the URL-data read can move: it's cheaper than a per-link prefetch and also covers hard load. Per-link prefetching is only for URL-data reads that genuinely can't move, or routes whose useful content is all URL-specific. **Insight:** [dynamic data during prefetching](https://nextjs.org/docs/messages/instant-link-prefetch-partial). diff --git a/plugins/codex/plugins/next-skills/skills/next-partial-prefetching-adoption/SKILL.md b/plugins/codex/plugins/next-skills/skills/next-partial-prefetching-adoption/SKILL.md index d9f38018..0b0f784d 100644 --- a/plugins/codex/plugins/next-skills/skills/next-partial-prefetching-adoption/SKILL.md +++ b/plugins/codex/plugins/next-skills/skills/next-partial-prefetching-adoption/SKILL.md @@ -68,11 +68,11 @@ Then, for each one: export const prefetch = 'partial' ``` - If the route reads URL data (`params`, `searchParams`), the default link still warms only its skeleton (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section), so it's a runtime-prefetch candidate for step 5, not a finished adoption. Keep `prefetch={true}` on its links and mark the route: + If the route reads URL data (`params`, `searchParams`), the default link still warms only its skeleton (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section), so it's a per-link-prefetch candidate for step 5, not a finished adoption. Keep `prefetch={true}` on its links and mark the route: ```tsx - // TODO(runtime-prefetch): assess with the user whether URL data should resolve before click. - // See: https://nextjs.org/docs/app/guides/runtime-prefetching + // TODO(per-link-prefetch): assess with the user whether URL data should resolve before click. + // See: https://nextjs.org/docs/app/guides/optimizing-prefetching export const prefetch = 'partial' ``` @@ -87,13 +87,13 @@ Then, for each one: Once every audited destination has `prefetch = 'partial'`, finish in two moves. 1. **Enable the flag globally.** Set `partialPrefetching: true` in `next.config.ts` (alongside `cacheComponents: true`). Every route is adopted now, so every link is good. -2. **Strip the redundant `prefetch = 'partial'` exports.** Run the first-party `remove-partial-prefetch` codemod rather than a text find-and-replace. It removes only `export const prefetch = 'partial'` and its generated Partial Prefetching guide comment. It leaves other values such as `prefetch = 'force-disabled'` in place, along with your `TODO(runtime-prefetch)` markers and their Runtime Prefetching guide links, which wait for step 5. +2. **Strip the redundant `prefetch = 'partial'` exports.** Run the first-party `remove-partial-prefetch` codemod rather than a text find-and-replace. It removes only `export const prefetch = 'partial'` and its generated Partial Prefetching guide comment. It leaves other values such as `prefetch = 'force-disabled'` in place, along with your `TODO(per-link-prefetch)` markers and their Optimizing prefetching guide links, which wait for step 5. ```bash npx @next/codemod@latest remove-partial-prefetch ./app ``` - The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand by removing `export const prefetch = 'partial'` and its generated Partial Prefetching guide comment from every `app/**/{page,layout}.{js,jsx,ts,tsx}` — leave other `prefetch` values in place, and leave the `TODO(runtime-prefetch)` markers and Runtime Prefetching guide links where they are. Don't hand-edit when the codemod can run. + The codemod refuses to run on a dirty working tree. Commit or stash unrelated work first, or pass `--force` to let its edits land alongside your WIP. If the codemod isn't available (older `@next/codemod`, sandboxed environment, offline run), reproduce it by hand by removing `export const prefetch = 'partial'` and its generated Partial Prefetching guide comment from every `app/**/{page,layout}.{js,jsx,ts,tsx}` — leave other `prefetch` values in place, and leave the `TODO(per-link-prefetch)` markers and Optimizing prefetching guide links where they are. Don't hand-edit when the codemod can run. ## step 3: sweep for URL-data insights (after enabling) @@ -116,24 +116,24 @@ Checklist before checking in with the user: - **An empty sweep is expected when Cache Components adoption finished cleanly.** A quiet log is success, not a missing signal. If you deliberately probe the validation path, use a `generateStaticParams` route with `params` read inside `` but before the URL-specific leaf boundary; other shapes may surface `blocking-prerender-*` instead. - The App Shells are real: for each route you changed, confirm the first paint after a navigation shows the intended shared content, not an empty shell or a stuck fallback. A `` around the whole page body passes validation with an empty shell, which defeats the point. - The insights validate shell _structure_, not that a prefetch actually happened. Confirm on the production run (prefetching is prod-only) that navigating a changed link lands on the shared shell instantly. -- **If the app prefetches imperatively**, the insight sweep does not cover it, so an empty sweep is not proof the prefetch survived the flag. Verify the call under `next start`: compare the `_rsc` prefetch response or resource timing before/after, and make sure any intentionally preserved full prefetch still carries the data the old call was warming. If it now returns only the App Shell, migrate that call site using the same decision as the nearest `` destination — cache the data, or move runtime-prefetch behavior to a docs-supported ``. +- **If the app prefetches imperatively**, the insight sweep does not cover it, so an empty sweep is not proof the prefetch survived the flag. Verify the call under `next start`: compare the `_rsc` prefetch response or resource timing before/after, and make sure any intentionally preserved full prefetch still carries the data the old call was warming. If it now returns only the App Shell, migrate that call site using the same decision as the nearest `` destination — cache the data, or move per-link-prefetch behavior to a docs-supported ``. - **Before blaming a broken route on the flag, reproduce it with `partialPrefetching` off** (or on the pre-flag branch). The flag surfaces existing issues — a fragile request-time auth gate, a rewrite, deployment skew — earlier and more visibly, but rarely causes them. If it breaks flag-off too, it isn't a Partial Prefetching problem; fix it there, not here. - `next build` still passes. Then check in with the user. Speak their language — no insight slugs or step labels. - What you did: which links you audited, which destinations you adopted, and what each link now prefetches. -- What changed: dropped props, `use cache` boundaries added, and which routes carry a `TODO(runtime-prefetch)` marker for later. +- What changed: dropped props, `use cache` boundaries added, and which routes carry a `TODO(per-link-prefetch)` marker for later. - Demo against a production run. Prefetching is limited in development, so `next dev` won't show the result — run `next build` and `next start`, and hand the user that URL. That run needs the app's real environment (database, auth, secrets), and a partial or stale install or leftover generated artifacts can fail the build for reasons unrelated to the adoption. Set the expectation up front that verification is a complete, credentialed production run, not a quick check. - Show, don't tell: drive one link live in the headed browser against the production server, so they see the shared App Shell paint instantly and the URL-specific region stream in. Attach before/after screenshots only when a live browser isn't possible. - Give them the click-through: a table of each changed route — the link to click, and what to expect after the click (what paints instantly, what streams in) — so they can verify each result themselves. -- The question: "Want to commit this (or open the PR) before we look at which routes should also prefetch their URL-specific content?" Wait for the answer — adoption and runtime prefetching read best as their own changes. +- The question: "Want to commit this (or open the PR) before we look at which routes should also prefetch their URL-specific content?" Wait for the answer — adoption and per-link prefetching read best as their own changes. -## step 5: runtime prefetching (optional) +## step 5: per-link prefetching (optional) -The audit marked the candidates instead of deciding them. Grep for `TODO(runtime-prefetch)` and walk the list with the user in one conversation. The question per route is whether they want the URL-dependent content prefetched ahead of the click, or streaming in after navigation is fine. A runtime prefetch costs a server invocation per prefetchable link — the guide's [per-link prefetching trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) section is the checklist. Don't make these calls alone. +The audit marked the candidates instead of deciding them. Grep for `TODO(per-link-prefetch)` and walk the list with the user in one conversation. The question per route is whether they want the URL-dependent content prefetched ahead of the click, or streaming in after navigation is fine. A per-link prefetch costs a server invocation per prefetchable link — the guide's [trade-offs](https://nextjs.org/docs/app/guides/optimizing-prefetching#trade-offs) section is the checklist. Don't make these calls alone. -Where the answer is yes, follow the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching): keep [``](https://nextjs.org/docs/app/api-reference/components/link#prefetch) on the links that should resolve more than the App Shell, and cache the content behind the URL-data read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Each per-link prefetch is a server render when the destination needs non-static data, so use the guide's [per-link trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) to decide when viewport prefetching is worth it and when [hover-triggered prefetch](https://nextjs.org/docs/app/guides/prefetching#hover-triggered-prefetch) is a better fit. Where it's no, delete the marker and leave the route on the App Shell default. Either way no `TODO(runtime-prefetch)` marker survives this step. Confirm the opted-in links against a production run (`next build` and `next start` — the runtime prefetch fires there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR. +Where the answer is yes, follow the [Optimizing prefetching guide](https://nextjs.org/docs/app/guides/optimizing-prefetching): keep [``](https://nextjs.org/docs/app/api-reference/components/link#prefetch) on the links that should resolve more than the App Shell, and cache the content behind the URL-data read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Each per-link prefetch is a server render when the destination needs non-static data, so use the guide's [per-link trade-offs](https://nextjs.org/docs/app/guides/optimizing-prefetching#trade-offs) to decide when viewport prefetching is worth it and when [hover-triggered prefetch](https://nextjs.org/docs/app/guides/prefetching#hover-triggered-prefetch) is a better fit. Where it's no, delete the marker and leave the route on the App Shell default. Either way no `TODO(per-link-prefetch)` marker survives this step. Confirm the opted-in links against a production run (`next build` and `next start` — the per-link prefetch runs there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR. ## further reading diff --git a/plugins/codex/plugins/ppt-master/README.md b/plugins/codex/plugins/ppt-master/README.md index 0292425a..52c4ca74 100644 --- a/plugins/codex/plugins/ppt-master/README.md +++ b/plugins/codex/plugins/ppt-master/README.md @@ -13,7 +13,7 @@ English | [中文](./README_CN.md)
-This project is kept free and open source with the support of Kimi, PackyCode, APIKEY.FUN, RunAPI, YouYun ZhiSuan and other sponsors. +This project is kept free and open source with the support of Kimi, PackyCode, APIKEY.FUN, RunAPI, YouYun ZhiSuan and other sponsors.

Kimi @@ -35,8 +35,8 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring this Thanks to APIKEY.FUN for sponsoring this project! APIKEY.FUN is a professional enterprise-grade AI relay service committed to stable, efficient, and low-cost AI access for businesses and developers. The platform supports mainstream models including Claude, OpenAI, and Gemini, with prices as low as 7% of official rates. Register through our dedicated link for an exclusive perk: up to 5% off on top-ups, permanently. - RunAPI - Thanks to RunAPI for sponsoring this project! RunAPI is an efficient and stable API platform — a single API Key gives you access to 150+ leading models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, at prices as low as 10% of official rates, with exceptional stability and seamless compatibility with tools like Claude Code. RunAPI offers an exclusive perk for PPT Master users: register and contact an administrator via our dedicated link to claim ¥7 in free credit. + RunAPI + Thanks to RunAPI for sponsoring this project! RunAPI is an efficient and stable API platform — a single API Key gives you access to 150+ leading models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, at prices as low as 10% of official rates, with exceptional stability and seamless compatibility with tools like Claude Code. RunAPI offers an exclusive perk for PPT Master users: register and contact an administrator via our dedicated link to claim ¥7 in free credit. YouYun ZhiSuan @@ -235,7 +235,7 @@ Never used one of these? Don't worry — in this project they play exactly one r > **Model recommendation**: for the best results, use **[Kimi K3](https://www.kimi.com/code/?aff=ppt-master)** (or Claude) to drive the pipeline, paired with AI image generation — **`gpt-image-2`** (OpenAI) or **`gemini-3.1-flash-image`** (Google). Kimi Code, the project sponsor, is a great pick for pay-as-you-go access. -**🔑 Want to use Claude / GPT / Gemini but don't have access yet?** Project sponsors **[PackyCode](https://www.packyapi.ai/register?aff=ppt-master)**, **[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER)** and **[RunAPI](https://runapi.co/register?aff=WMLJ)** offer pay-as-you-go access to Claude, GPT, Gemini and more — no subscription required, with exclusive discounts for our users (details at the top of this page). +**🔑 Want to use Claude / GPT / Gemini but don't have access yet?** Project sponsors **[PackyCode](https://www.packyapi.ai/register?aff=ppt-master)**, **[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER)** and **[RunAPI](https://runapi.host/register?aff=WMLJ)** offer pay-as-you-go access to Claude, GPT, Gemini and more — no subscription required, with exclusive discounts for our users (details at the top of this page). **🔀 Juggling several providers?** Once you hold keys from more than one of them, [cc-switch](https://github.com/farion1231/cc-switch) — a cross-platform desktop app — lets you one-click switch API providers for Claude Code, Codex, Gemini CLI and more, no manual config editing. @@ -414,7 +414,7 @@ PPT Master is currently built and maintained primarily by me. Every new template   APIKEY.FUN   -RunAPI +RunAPI   YouYun ZhiSuan   diff --git a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json index 3a56b3d8..fb2c387f 100644 --- a/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json +++ b/plugins/codex/plugins/ppt-master/THIRD_PARTY_SOURCE.json @@ -2,8 +2,8 @@ "sourceId": "ppt-master", "repo": "https://github.com/hugohe3/ppt-master.git", "ref": "main", - "commit": "182c6b8229a44990cdc5b394545f90992be377d6", + "commit": "4e6ecbcb0dc079efebd3c79b775c0f02581509fe", "adapter": "claude-skill", "sourcePath": "skills/ppt-master", - "syncedAt": "2026-08-10T15:59:59Z" + "syncedAt": "2026-08-11T16:00:01Z" } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md index d8865e77..817227a6 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS.md @@ -36,9 +36,9 @@ Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring PPT M ### RunAPI -RunAPI +RunAPI -[RunAPI](https://runapi.co/register?aff=WMLJ) provides access to 150+ models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, through one API key. Register through the dedicated link and contact an administrator to claim **¥7 in free credit**. +[RunAPI](https://runapi.host/register?aff=WMLJ) provides access to 150+ models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, through one API key. Register through the dedicated link and contact an administrator to claim **¥7 in free credit**. ### YouYun ZhiSuan diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md index ec735218..10f395cc 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/SPONSORS_CN.md @@ -36,9 +36,9 @@ PPT Master 始终免费开源。以下赞助方共同支持项目的持续维护 ### RunAPI -RunAPI +RunAPI -[RunAPI](https://runapi.co/register?aff=WMLJ) 通过一个 API Key 提供 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型。通过专属链接注册并联系管理员,即可领取 **¥7 免费额度**。 +[RunAPI](https://runapi.host/register?aff=WMLJ) 通过一个 API Key 提供 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型。通过专属链接注册并联系管理员,即可领取 **¥7 免费额度**。 ### 优云智算 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md index c7f82d40..921d6648 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/animations.md @@ -13,11 +13,14 @@ before the page plan is frozen, not only when a deck is already exported. | What the deck needs | Reach for | Decided at | |---|---|---| -| Reveal content in step with the narration | Per-element object animation — `-a auto` for generic entrance reveals, or an `animations.json` sidecar for explicit enter/emphasize/move/exit/static lifecycle choreography | Post-processing; §2, §4, [`customize-animations`](../workflows/stages/customize-animations.md) | +| A generic deck-wide entrance build | `-a auto`; with the default `after-previous` Start mode, groups use fixed `--animation-stagger` timing rather than narration cues | Post-processing; §2, §4 | +| Explicit object lifecycle choreography | An `animations.json` sidecar for selected enter/emphasize/move/exit/static duties, order, Start mode, and timing | Post-processing; §2, §4, [`customize-animations`](../workflows/stages/customize-animations.md) | +| Object reveals semantically synchronized to recorded narration | Narration-cue sync derives `narration_animations.json` from canonical `animations.json`, page-local SRT, and `narration_timing.json`; `-a auto` alone does not provide this mapping | Audio stage; [`generate-audio`](../workflows/stages/generate-audio.md) | | A continuous action — slide-in, flip, camera push-in, progressive reveal, camera pan | **Morph: author the action as two static pages, then select Morph and add explicit pairs when identity must be deterministic.** There is no keyframe timeline anywhere in this pipeline; the difference between two ordinary editable slides *is* the animation | **Page authoring (Step 6), then motion post-processing** — §2.1, §3.1 | | A static full-bleed page that should stop looking frozen | Consider slow `path_*` motion on a visually subordinate image or atmospheric layer; §4.1 gives one starting recipe | Post-processing; §4.1 | | Carousel, counting numerals, parallax depth, click-to-reveal flip card | Four recurring recipes assembled from the mechanisms above | §4.2 — the carousel and odometer both need paired pages | | Kiosk or unattended playback | `--auto-advance `, optionally with `-t none` | Export; §3 | +| A transition or object animation needs an audible cue | Optional `transition.sound` or object-animation `sound`; select it only after the visual motion solution is complete, then sync the chosen global-library ids into the project. For direct narrated MP4 delivery, [`generate-audio`](../workflows/stages/generate-audio.md) selects either the verified native-export mix or explicit real-time slideshow capture; never combine them | Post-motion; §2.2 | | Nothing should move | `-t none`, and leave per-element animation at its default `none` | Export; §1 | **Hard rule — Morph geometry is an authoring decision; pairing is a later @@ -40,6 +43,7 @@ tell; each capability above earns its place per page, not per deck. |---|---|---| | Page transition | CLI: `fade`, 0.4s | Calm baseline that suits most decks; the public Python builder retains its legacy 0.5s default | | Per-element animation | **`none` (off)** | A page appears as a whole. Auto-firing element builds are an unsolicited "AI deck" tell, so object animation is opt-in. Turn on the content-aware canonical entrance policy with `-a auto`, or select one PowerPoint-native `entrance_*`, `emphasis_*`, `path_*`, or `exit_*` key explicitly | +| Sound effects | **`none` (off)** | No global sound is copied and no `/sounds/` directory is created unless a resolved transition or object-animation cue actually selects one | To regenerate a deck with different settings, rerun the final checker when its current matching report is absent or stale, then rerun `svg_to_pptx.py` against the same `svg_output/`; the content-generation LLM need not rerun unless authored SVG requires repair. `-s final` is reserved for diagnostic comparison and is not a supported release source. To turn per-element animation on for the whole deck, pass `-a auto`. @@ -155,7 +159,9 @@ Rules: `decelerate`; `restart` is `always`, `when-not-active`, or `never`; `after_effect` is `none`, `dim` (with `color`), `hide`, or `hide-on-next-click`; `sound` is a project-relative or absolute `.m4a`, - `.mp3`, or `.wav` path. + `.mp3`, or `.wav` path. New generated configurations use a project-relative + path. A bundled library choice first follows §2.2 and resolves to a + project-local `.wav`; never point new output at `templates/sounds/`. - `Speed` and smooth start/end are not duplicate sidecar fields: they are derived from `duration` and `accelerate`/`decelerate`. - This is the complete parameter surface for the generated top-level-group @@ -238,6 +244,51 @@ The generated names follow Microsoft's OOXML object types. Missing, structural, moved, ambiguous, or mismatched targets fail instead of falling back to automatic Morph matching. +### 2.2 On-Demand Sound Selection + +**Hard rule — select after motion, materialize after selection**: sound is not a +Strategist resource and does not belong in `design_spec.md`, `spec_lock.md`, or +pre-SVG resource preparation. First complete the SVG roster and resolve the +transition/object-motion solution. Only when a specific cue is then selected, +copy its global-library file into the project and reference that local copy. + +| Source | Action | +|---|---| +| Bundled CC0 library | Discover ids with `sound_sync.py list`, sync only the selected ids, then use the corresponding `sounds//.wav` paths | +| User-provided audio already inside the project | Reference its existing project-relative `.m4a`, `.mp3`, or `.wav` path for object animation; a transition sound uses `.wav` | +| External absolute file | The low-level object-animation path remains compatible, but new generated projects should copy or sync the intended file into the project and use a relative path | +| No concrete auditory cue job | Keep `sound` omitted; do not create `/sounds/` and do not copy the library | + +```bash +# Inspect ids only after the visual motion solution exists +python3 skills/ppt-master/scripts/sound_sync.py list +python3 skills/ppt-master/scripts/sound_sync.py list --query + +# Materialize only the chosen ids +python3 skills/ppt-master/scripts/sound_sync.py \ + / [/ ...] +``` + +`sound_sync.py` is the only bundled-library materialization path. Stable ids +include their namespace; copied files remain under +`/sounds//`. The exporter never reads the global +`templates/sounds/` library directly, and sidecars store paths rather than +library ids. + +**Default — silence (may override for a specific cue)**: do not add sound to +demonstrate capability or spread it across a deck for coverage. A sound may +support a named transition, reveal, confirmation, warning, or drawn/moving +gesture after the corresponding visual behavior is already selected. + +**Hard rule — PPTX and MP4 are separate sound deliveries**: sound fields and +package read-back prove the editable PPTX contains the intended native cue; +they do not prove PowerPoint's video encoder placed it in the MP4 audio track. +For direct narrated MP4 delivery with resolved cues, follow `generate-audio` +and choose exactly one branch: mix from the final narrated trace plus final +PPTX after native encoding, or explicitly capture the live PowerPoint Slide +Show with system audio. Never mix the capture again. Keep post-production gain +and limiter settings out of `animations.json`. + --- ## 3. Page Transitions @@ -304,6 +355,13 @@ carry a real PowerPoint effect in `mc:Choice` and a `fade` fallback for older consumers; validation requires the requested primary effect and never accepts the fallback as a silent substitute. +An optional `transition.sound` adds one `.wav` cue to the transition. It is a +sidecar field rather than a CLI flag. Bundled choices must first be synced by +§2.2 and referenced through their project-relative path. `effect: none` may +still carry a transition sound and/or automatic advance without restoring a +visual effect. A slide-level `transition.sound: null` explicitly clears an +inherited default transition sound for that page. + Flags: - `-t/--transition` — native effect name, compatibility input, or `none` for no visual transition. Default: `fade`. `none` does not remove an explicitly configured automatic advance. @@ -441,7 +499,7 @@ Flags: `-a/--animation` selects effect/mode; `--animation-trigger` selects Start `--animation-config` selects a sidecar; `--no-animations` disables page/object motion but preserves narration audio and recorded advance timing. -> Note: `--recorded-narration` rejects `on-click` and `trigger_shape`. When either animation sidecar exists, narrated export selects `narration_animations.json`; canonical `animations.json` without that derived file remains a synchronization error. Without sidecars, pass `--inherit-motion-from ` for the base deck motion. Pass `--animation-config animations.json` for canonical animation, or `--no-animations` to remove page and object motion. +> Note: `--recorded-narration` rejects `on-click` and `trigger_shape`. Narration-cue sync uses `narration_animations.json` and blocks when only canonical `animations.json` exists. Narration-independent custom motion explicitly passes `--animation-config animations.json`, even when a derived sidecar also exists. With no sidecar, pass `--inherit-motion-from `; explicit all-motion-off uses `--no-animations`. ### 4.1 Slow ambient motion — the page that breathes @@ -537,6 +595,12 @@ object-animation timing before and after their allowed edits, then run structural package validation; they do not author or normalize animation effects. +**Validation boundary**: these checks prove PPTX timing, relationships, and +embedded sound parts. They are not final-video audio acceptance. The +native-export branch requires a triggered `video_sound_mix.py` receipt; the +slideshow-capture branch requires the human picture/audio/all-cue acceptance +owned by `generate-audio` and never claims that receipt. + --- ## 7. Video Adaptation Contract @@ -547,6 +611,13 @@ locks identity, order, effect, direction, and timing; video may refine only its declared renderer parameters. Unsupported families fail visibly. See [`video-motion-plan.md`](../scripts/docs/video-motion-plan.md). +On the native-export mix branch, direct narrated video sound uses the final +resolved trace for cue order and offsets, the final PPTX relationships for the +exact embedded audio bytes, and page-level narration correlation for the +exported-video clock. It never reads sound timing from a raw sidecar or +filename. The explicit slideshow-capture branch records PowerPoint's real-time +playback instead and does not consume the trace for sound mixing. + --- ## 8. Limitations @@ -556,6 +627,11 @@ declared renderer parameters. Unsupported families fail visibly. See not create object anchors. - PowerPoint OOXML is the compatibility target; other presentation apps may reinterpret individual native behavior trees. +- PowerPoint's native MP4 encoder may omit transition and object-animation + sounds even when the PPTX package is valid. Direct sound-enabled MP4 delivery + therefore uses either the post-export mix or the explicit real-time + slideshow-capture contract owned by `generate-audio`; the branches never + stack. - Direct-PPTX routes preserve unknown transition `AlternateContent`; timing edits keep Choice and Fallback advance attributes synchronized. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md index 9f8b1b0b..9fab0d03 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-base.md @@ -13,7 +13,7 @@ Always-loaded Executor authority for flat SVG page authoring and behavior shared | Any semantic cell grid, including a table-family reference | [`executor-table.md`](./executor-table.md) | | A page will use a preset pattern fill or an independent object is explicitly selected as native-ready | [`native-data-interface.md`](./native-data-interface.md) before emitting the pattern or replacement metadata | | Any image/formula | [`executor-image.md`](./executor-image.md) + [`image-layout-spec.md`](./image-layout-spec.md) + [`image-layout-patterns.md`](./image-layout-patterns.md) + [`svg-image-embedding.md`](./svg-image-embedding.md) | -| Any `Status: Sourced` web image | [`executor-web-image.md`](./executor-web-image.md), after `executor-image.md` | +| Any placed image is `Status: Sourced` or its filename has an `image_sources.json` record | [`executor-web-image.md`](./executor-web-image.md), after `executor-image.md` | | Effective Speaker Notes outcome is enabled after all SVG pages pass | [`executor-notes.md`](./executor-notes.md) | Evaluate branches from each object's actual information model, not only from a Chart/Table reference. A catalog family selects construction guidance but never native readiness; `Native-ready` is an independent object-level decision. Page-local qualitative geometry also never implies package-level `pptx_structure.mode: structured`. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md index 7164eae1..bad57955 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-image.md @@ -12,12 +12,12 @@ Conditional Executor authority for image status handling, placement, crop behavi Handle images by status; enum and lifecycle: [`svg-image-embedding.md`](svg-image-embedding.md). -**Mode boundary**: Default keeps Strategist → Executor with no downstream acquisition/reselection. Quick substitutes the main agent's prepared active-context resources for §VIII/lock below; the same boundary starts at SVG authoring. +**Mode boundary**: Default Executor consumes only prepared §VIII/lock assets. Quick enters the same consume-only boundary at SVG authoring from prepared active-context resources. Derivatives already exist; native treatments remain SVG work. | Status | Source | Handling | |--------|--------|----------| | **Existing** | User-provided | Reference images directly from `../images/` directory | -| **Generated** | Generated by Image_Generator | Reference images directly from `../images/` directory | +| **Generated** | Generated by Image_Generator | Reference from `../images/`; manifest-backed files load [`executor-web-image.md`](./executor-web-image.md) | | **Sourced** | Web-acquired by Image_Searcher | Reference from `../images/`. **Read [`image_sources.json`](image-searcher.md) to decide attribution** — load [`executor-web-image.md`](./executor-web-image.md). | | **Rendered** | Deterministic formula PNG | Reference from `../images/`; use a legal anchor with `meet` (centered default: `xMidYMid meet`) | | **Needs-Manual** | Acquisition or suitability remains unresolved | Default uses a placeholder until Step 7. Quick blocks every required row in this status; file presence alone does not bypass it. | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-web-image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-web-image.md index 49fdf497..aa10327e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-web-image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/executor-web-image.md @@ -2,13 +2,13 @@ # Executor Web-image Attribution Branch -Conditional Executor authority for inline attribution on web-sourced images. +Conditional Executor authority for inline attribution on web-sourced images and their prepared derivatives. -**Trigger**: load for any placed `Status: Sourced` image. Quick Generate uses the same `image_sources.json` contract without interaction. +**Trigger**: load for any placed `Status: Sourced` image or any placed prepared derivative whose filename has a copied `image_sources.json` record. Quick Generate uses the same manifest contract without interaction. ## 1. Inline Attribution for Sourced Images -Whenever the slide uses an image with `Status: Sourced`, look up the corresponding entry in `project/images/image_sources.json` and act on `license_tier`: +Whenever the slide uses a `Status: Sourced` image or a prepared derivative backed by `image_sources.json`, look up the corresponding filename entry and act on `license_tier`: | `license_tier` | Action on this slide | |---|---| diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md index aa5765cb..ae15b0f3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-base.md @@ -1,6 +1,6 @@ > See [`image-generator.md`](./image-generator.md) and [`image-searcher.md`](./image-searcher.md) for path-specific behavior. -# Image Acquisition Common Reference +# Image Acquisition and Preparation Common Reference Shared baseline for both acquisition paths. Path-specific behavior lives in the path's own reference. @@ -8,7 +8,7 @@ Shared baseline for both acquisition paths. Path-specific behavior lives in the ## 1. Trigger Condition -Active when at least one resource row has `Acquire Via: ai` / `web` / `slice`. Rows with `user` / `formula` / `placeholder` are tracked but skipped by these acquisition roles. +Active when at least one resource row has `Acquire Via: ai` / `web` / `slice`, or when any §VIII / Quick active-context resource is a pending prepared derivative. Canonical rows with `user` / `formula` / `placeholder` are tracked but skipped by acquisition roles. | Mode | Trigger | |---|---| @@ -26,18 +26,43 @@ Default Generate uses Strategist-owned `design_spec.md §VIII` plus its lock pro |---|---|---|---|---|---|---|---| | `` | `` | `` | `` | `adaptive` / `no-crop` | `ai` / `web` / `slice` | Pending | `` | -**Required per non-skipped row**: `Acquire Via` and `Status`. `Reference` is required for every `web` / `slice` row and every newly authored `ai` row. An existing `ai` row whose `Reference` is omitted or blank may continue only through the declared inference in [`image-generator.md`](./image-generator.md) §8; no other path may infer it. +**Required per non-skipped row**: `Acquire Via` and `Status`. `Reference` is required for every `web` / `slice` row, every newly authored `ai` row, and every prepared derivative regardless of source class. An existing `ai` row whose `Reference` is omitted or blank may continue only through the declared inference in [`image-generator.md`](./image-generator.md) §8; no other path may infer it. **Quick Generate ownership**: explicit user assets, URLs, and path instructions win. Otherwise the main agent chooses required `user` / `ai` / `web` / `slice` / `formula` rows and AI path `auto`, without interaction. +**Mandatory — consume the resolved path**: Default consumes Strategist-chosen §VIII rows; Quick resolves once in active context before preparation. This phase never adds or reselects a treatment: + +| Path | Behavior | +|---|---| +| `none` | Use the canonical bitmap unchanged | +| `native` | No new file; SVG owns crop/clip, transform, opacity, frame/shadow/scrim/vignette, and overlap | +| `prepared derivative` | Separate file only for pixel blur, desaturation/grayscale, duotone, brightness/contrast, or existing cutout/registered-layer preparation | + +Choosing `none` is valid. Never bake a native treatment into a derivative. + +**Reference — exact pattern mapping, not activation**: An adopted pattern may use this mapping; its id alone creates nothing. + +| Pattern | Preparation | +|---|---| +| `P*`, `M*`, `C*` | Use existing assets with native SVG/PPT composition; no automatic derivative | +| `A1-02` / `A1-03` | `image_treat.py` blur / duotone | +| `A1-01` / `A1-04` | Existing prepared composite or host/AI path; `image_treat.py` does not blend | +| `A2-01` | Existing/host-prepared RGBA or flat-key AI/slice asset; when source-scene registration is required, use `A2-02` / `A2-03` + [`image-generator.md`](./image-generator.md) §4.4 | +| `A2-02` / `A2-03` | [`image-generator.md`](./image-generator.md) §4.4 registered layers | +| `A3-01` | Original/subject plus registered `image_treat.py` blur/tone/desaturate derivative | +| `A3-02` | Registered full-canvas `image_treat.py` blur derivative; crop panels natively | +| `A3-03` | `image_treat.py` desaturated base plus existing/§4.4 color subject layer | + --- ## 3. Path Dispatch -For each row with `Status: Pending`: +Classify `Reference: Derived from ; treatment=; ...` before `Acquire Via`. Its distinct, non-derived parent must be `user`, `web`, `ai`, or `slice`; reject formula/placeholder parents, chains, cycles, and self-reference. For each Pending row: -| Acquire Via | Load reference | Run | Success status | +| Row kind / Acquire Via | Load reference | Run | Success status | |---|---|---|---| +| Deterministic prepared derivative | This common reference | After parent is usable, run `image_treat.py` to a distinct `.png`; preserve source | Inherit parent: `user → Existing`, `web → Sourced`, `ai/slice → Generated` | +| Registered-layer derivative | [`image-generator.md`](./image-generator.md) §4.4 | After parent is usable, run §4.4 | Supplied final: `user → Existing`; generated/reconstructed: `ai → Generated` | | `ai` | [`image-generator.md`](./image-generator.md) | `image_gen.py` | `Generated` | | `web` | [`image-searcher.md`](./image-searcher.md) | `image_search.py` | `Sourced` | | `slice` | [`image-generator.md`](./image-generator.md) §4.3 | `slice_images.py` after parent AI sheet is `Generated` | `Generated` | @@ -51,12 +76,10 @@ For each row with `Status: Pending`: ## 4. Analysis Phase -Before processing any row: - -1. Read the Default Design Spec/lock, or reuse Quick's active-context resource and visual/page decisions -2. Group resource list rows by `Acquire Via` -3. Confirm `project/images/` exists -4. Materialize explicit user assets, render declared formulas, and finish triggered ai/web/slice acquisition before SVG authoring begins +1. Read the Default Design Spec/lock, or reuse Quick's active-context resource/page decisions. +2. Separate derivatives before grouping canonical rows by `Acquire Via`; ensure `project/images/` exists. +3. Finish user/formula and triggered ai/web/slice canonical preparation. +4. Materialize only declared derivatives from usable parents, preserve originals, then run `analyze_images.py` once before SVG. --- @@ -65,6 +88,7 @@ Before processing any row: After all rows reach terminal status: - Every non-skipped row has a file at `project/images/`, or is marked `Needs-Manual` +- Each derivative has its distinct file and usable parent; web provenance is copied in `image_sources.json` - Every `slice` row has a generated element file, or is marked `Needs-Manual` because its parent sheet is not available - No `Pending` or `Failed` rows remain - `image_prompts.json` exists when ≥1 ai row processed; every entry has `status ∈ {Generated, Needs-Manual}` (no `Pending` or `Failed` remaining) @@ -72,7 +96,7 @@ After all rows reach terminal status: > `Needs-Manual` is terminal for acquisition, not export readiness. A later > supplied/replaced file must be validated and its row reconciled to -> `Generated`, `Sourced`, or `Rendered` with the matching manifest evidence. +> `Existing`, `Generated`, `Sourced`, or `Rendered` with matching evidence. > Quick blocks every required row that still says `Needs-Manual`, regardless of > whether an unverified candidate file happens to exist. See > [`image-generator.md`](./image-generator.md) §7. @@ -118,6 +142,8 @@ Executor reads the manifest per slide and renders inline credits when needed — The `Reference` field is **intent**, not a query. Strategist owns it by default; Quick's main agent resolves it in active context. The receiving role translates without reopening it. +For derivatives, the required lineage/treatment prefix is intent metadata, not a provider query. + | ✅ Intent | ❌ Pre-processed | |---|---| | `"Diverse engineering team in modern office, natural light"` | `"team office light"` | @@ -132,11 +158,11 @@ SVG authoring consumes the active profile's resource authority plus: | Artifact | Path | Purpose | |---|---|---| | Image files | `project/images/*.{jpg,png,webp}` | `` references | -| Manifest | `project/images/image_sources.json` | `license_tier` per Sourced image | +| Manifest | `project/images/image_sources.json` | License/provenance for sourced images and their derivatives | -**Default Generate boundary**: Executor does NOT invoke `image_gen.py` / `image_search.py` / `slice_images.py`; missing material returns to Strategist-owned preparation. +**Default Generate boundary**: Executor does NOT invoke `image_gen.py` / `image_search.py` / `slice_images.py` / `image_treat.py`; missing material returns to Strategist-owned preparation. -**Quick Generate boundary**: the main agent finishes acquisition before SVG authoring, then neither acquires nor reselects while drawing. +**Quick Generate boundary**: the main agent finishes acquisition and planned derivation before SVG authoring, then neither acquires, derives, nor reselects while drawing. --- diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md index 0ba94dfc..488fc271 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/image-layout-patterns.md @@ -165,7 +165,7 @@ The following three patterns are topologically different and are not interchange ## 5. Asset-Dependent Treatments -**Prepared-asset gate**: every treatment below consumes its named project-local asset; it does not authorize creation during SVG realization. Embedded lettering belongs to the artwork only when deliberately fixed; authoritative or editable labels remain native SVG. If a required asset is absent, return to the active workflow's preparation owner or choose a native treatment. +**Prepared-asset gate**: every treatment below consumes its named project-local asset; it does not authorize creation during SVG realization. Embedded lettering belongs to the artwork only when deliberately fixed; authoritative or editable labels remain native SVG. If a required asset is absent, return to the active workflow's preparation owner or choose a native treatment. [`image-base.md`](./image-base.md) §2 maps these ids to preparation paths without auto-triggering them. ### 5.1 A1 · Prepared Composites and Appearance diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md index 59cb0e07..8e6edcfe 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist-image.md @@ -18,7 +18,9 @@ For illustration, apply this precedence: confirmed `none` → explicit user inte **Default — one coherent sheet for compatible same-family spots (may override when aspect, detail, quality, or semantic needs differ)**: prefer one Illustration Sheet when several AI-generated spots can share a useful cell shape and production treatment; generate them independently when forcing one sheet would weaken a planned element. When a sheet is chosen, plan one unplaced `ai` Illustration Sheet row plus one placed `slice` row per used element; only slice rows enter `spec_lock.md images`. State the intended placement shape family in the sheet reference and use separate sheets for incompatible shapes. [`image-generator.md`](./image-generator.md) §4.3 owns grid, ratio, slicing, and execution details. Final Stage 2 chooses the AI execution path under `image-generator.md` §7; do not pre-empt or re-pick it here. -**Mandatory — subject-layer capability scan**: When a supplied reference or the intended composition shows a subject crossing a native title, panel, frame, or shape, plan a registered reconstruction group before writing §VIII. Add one clean full-canvas base row and the minimum same-canvas RGBA midground/subject/foreground rows, give full-canvas members `Crop Policy: no-crop`, name their shared source and registration relationship in `Reference`, and suggest `#A2-03`. Padded-bbox-disjoint objects may share one generated plate if the final SVG gives each one an independent picture crop. Use `Acquire Via: user` only when every final asset is already supplied; otherwise use `ai` for the prepared derivatives. [`image-generator.md`](./image-generator.md) §4.4 owns preparation. A simple floating cutout that does not re-layer over its source may use `#A2-01` instead. +**Mandatory — image-treatment path scan, not a quota**: Per selected image choose `none` (unchanged), `native` (SVG crop/clip, transform, opacity, frame/depth, overlap), or `prepared derivative` (separate pixel blur/tone or cutout/registered layers); `none` is valid. + +When a subject crosses a native title, panel, frame, or shape, the prepared path is mandatory: plan a clean full-canvas base plus minimum registered RGBA layers; set full-canvas members `no-crop`; name their shared source/registration in `Reference`; suggest `#A2-03`. A shared plate requires padded-bbox-disjoint objects and independent final crops. Use `user` only when every final asset is supplied, otherwise `ai`; [`image-generator.md`](./image-generator.md) §4.4 owns preparation. An independent floating cutout may use `#A2-01`. ## 2. AI Image Strategy — always propose three; lock only for confirmed `ai` @@ -53,7 +55,9 @@ Follow `latex_render.py --help` for the manifest fields. The renderer writes dim ## 4. Image Resource List -Add §VIII rows for the image resources actually planned from the confirmed source boundary and for every selected formula; a formula-only plan contains only formula rows. A permitted but unused source needs no row. Author each row's filename, dimensions/ratio, preferred layout suggestion, crop policy, purpose/type, acquisition, status, reference, and conditional AI fields as part of the complete Design Spec. `Acquire Via` is `ai`, `web`, `user`, `formula`, `placeholder`, or `slice`; status follows [`svg-image-embedding.md`](./svg-image-embedding.md). When a planned or explicitly required asset is not yet available, retain its row as `Pending` or `Needs-Manual`; never remove the row or change `Acquire Via` to make the Design Spec look complete. After §VIII passes final confirmation, project every placed row into `spec_lock.md images` as ` | source= | pattern= | crop=` and omit unplaced Illustration Sheets. `source` and `crop` preserve the exact confirmed §VIII text; `pattern` preserves the non-empty free-form suggestion, including any optional hierarchical catalog ids, while remaining preferred expression rather than locked geometry. +Add §VIII rows only for planned images and every selected formula; permitted unused sources create no row. Fill filename, dimensions/ratio, layout suggestion, crop, purpose/type, acquisition, status, reference, and conditional AI fields. `Acquire Via` is `ai`, `web`, `user`, `formula`, `placeholder`, or `slice`; status follows [`svg-image-embedding.md`](./svg-image-embedding.md). Keep any unavailable planned/required asset `Pending` or `Needs-Manual`; never delete or reclassify it to appear complete. After final confirmation, project each placed row into `spec_lock.md images` as ` | source= | pattern= | crop=` and omit unplaced source/sheet rows. Preserve exact confirmed `source`/`crop`; keep non-empty `pattern`, including optional catalog ids, as preferred expression rather than locked geometry. + +**Prepared derivatives**: Keep canonical; `Reference`: `Derived from ; treatment=;`. Deterministic child: distinct `.png`, inherits acquisition; §4.4 follows `user`/`ai` above. Lock placed children; [`image-base.md`](./image-base.md) §2–3 owns preparation. References describe visual intent: AI uses subject + intent + composition without repeating rendering or HEX; web records exact subject, view/mood, focal/quiet region, and crop safety with positive quality cues; Image_Searcher later derives a separate short, specific provider query without rewriting this locked intent, while complete entity names or necessary disambiguation may use more words; formula preserves source LaTeX and placement intent. Any subject direction, focal placement, quiet region, or overlay-safety requirement that must affect acquisition/generation belongs in `Reference` or the matching §IX block, not only in `Layout pattern`. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md index 5f0e9438..f0499ec9 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/strategist.md @@ -38,6 +38,8 @@ solution + production gate: Do not force communication intent into one catalog label; Stage 1 records composite intent in prose. Editable prose fields are recommendation drafts, not required inputs: confirmation preserves current text and blanks; never repopulate a cleared field. Stage 2 confirms narrative spine, reading density, page budget, visual system, image direction, production mechanics, and how any installed template should be used. It never chooses or installs a template. Inspect only project-local template spec/prototypes, present one editable application plan, and keep exporter reuse/adherence internal. First author exactly three complete, project-fit solution directions from the confirmed contract and source; only then project each direction into mode, visual style, color, type, icons, and generated-image rendering for lower-level adjustment. Every direction projects a project-specific `custom` mode, `custom` visual style, and `custom` generated-image rendering; the fixed catalogs remain conservative lower-level single-select alternatives. All three must be viable and distinguishable as whole solutions, but do not force safe / shifted / bold archetypes, different catalog bases, or artificial extremes. After all three bundles are complete, compare them against the confirmed contract and source, choose the strongest overall fit, and write its actual zero-based index as `design_directions.selected` (`0`, `1`, or `2`); array order never determines preference. Every direction carries a complete generated-image rendering candidate even when AI imagery is not recommended; `recommend.image_usage` independently decides whether AI is proposed. Generated images inherit deck colors—there is no second image palette. Proactive defaults are speaker notes `true`, custom animations `false`, and narration audio `false`; a prior explicit user instruction overrides the matching recommendation, and effective narration audio requires effective speaker notes. Author each stage once; same-stage edits update only visible browser state through documented deterministic dependencies, without another AI/backend recommendation. Launch/derive/wait mechanics live in [`generate-pptx.md`](../workflows/generate-pptx.md) Step 4; item specs keep `a`–`h`. +**Default — continuity-aware whole solution (may override when a scene reset communicates better)**: Within active-profile invariants and before recommending page count or production mechanics, judge whether adjacent explanation beats can remain within one recognizable mental map while a visible state changes. Where that choice lowers cognitive switching and motion has a named communication job, let it shape the solution's narrative spine, page rhythm, visual approach, and enabled notes/narration segmentation, and recommend the existing `proactive_custom_animations: true`. This is one positive signal, not the only reason to enable animation; absent it, retain the existing default or other valid evidence. Topic or wording repetition alone is insufficient. A `Motion suggestion` remains optional advice and never changes the effective outcome. + **Hard rule — Stage-1 source boundary**: Build the communication recommendation only from the current user request, source facts, conversation constraints, and project-initialization state. Author it before loading index summaries for a chat listing, and do not read any candidate spec, prototype, asset, or template-owned canvas. The same Stage-1 surface may display template controls, but their values are confirmation state, not recommendation evidence. Do not load or apply [`strategist-template.md`](./strategist-template.md) until Stage 1 is confirmed and the selected workspace has been installed for Stage 2. > **Execution discipline**: Step 3 is non-interactive candidate preparation. Stage 1 is the first BLOCKING checkpoint and closes communication plus template/free-design choice in one confirmation. Its receipt is intermediate and MUST NOT end the task or produce a final chat reply. In the same active run, install/fuse any selection, complete its handoff, author fresh Stage 2, and enter the final confirmation wait. After final confirmation, proceed without another pause unless spec refinement is enabled. @@ -462,6 +464,8 @@ required meaning in the visible page and confirmed presenter channel. **Recommendation signals**: derive the initial reading mode from the confirmed `audience`, `delivery_context`, and `artifact_afterlife`. Asynchronous review, reference, approval, audit, and leave-behind use lean `text`; presenter-led projection, large-room delivery, launch, or classroom explanation lean `presentation`; hybrid review / roadshow use leans `balanced`. When live projection and durable afterlife both matter, recommend `balanced` unless the contract clearly prioritizes one. If the user confirms `presentation`, support afterlife through enabled notes, appendix pages, captions, and visible sources instead of crowding every slide. +**Default — visible-state sequence (may override when a new composition is clearer)**: Before freezing the §IX roster and enabled notes/narration boundaries, compare adjacent semantic beats within the active profile's roster/content invariants. When recurring roles, relationships, and spatial orientation form one mental map and the next beat has a meaningful state or focus change, plan neighboring pages as visible states of that scene: preserve recognizable anchors, make the semantic delta legible, and align each enabled notes/narration segment with its supporting visible state. This is a content-and-rhythm strategy, not a page quota. Reset the composition when the mental map changes or continuity adds no clarity. Within the confirmed page count, every state page must carry content and an `Audience move`; the effective motion outcome changes realization, not roster authority. + **Per-block expression**: let the semantic relationship choose the form. Causal explanation, argument, interpretation, and narrative continuity use prose. Truly parallel, ordered, or enumerable items may use bullets / numbers. Never create bullets merely because copy is long or a template exposes a list slot. In `presentation`, distill one assertion and move its explanation into enabled notes rather than turning every sentence into a fragment; when notes are disabled, keep the necessary explanation in the visible page or confirmed presenter channel. Source texture remains a secondary cue: an article / transcript / talk leans prose, while a data sheet or inventory may lean structured labels. Write complete, usable phrasing into §IX; do not leave skeletons for Executor. It is preferred wording unless literal preservation applies; Executor owns faithful expression adaptation under [`executor-base.md`](./executor-base.md) §2.1's content-vs-expression contract. This is what makes the axis meaningful: a `presentation` deck and a `text` deck built from the **same source and communication contract** must differ in page grammar, page count recommendation, per-page text volume, visual burden, layout density, rhythm, and enabled notes—not only in font size. Page count stays the user's call; reading mode informs the recommendation when the user has not fixed one. Record it as **Reading Mode** in `design_spec.md §I` (compatibility key `delivery_purpose`, lock key `consumption_mode`). Separately, `communication_intent` / `audience_outcome` determine what the outline must accomplish, while `delivery_context` and `artifact_afterlife` help select the reading mode and still remain independent constraints after selection. The `page_rhythm` leans are a bias, not a quota. Preservation paths keep source wording and structure verbatim: honor reading mode only in styling and enabled notes, never by rephrasing or re-paginating. @@ -512,7 +516,7 @@ includes transitions. - **Custom behavior is concise and executable**: For confirmed `custom` mode or visual style, project one resolved `mode_behavior` / `visual_style_behavior` sentence or short paragraph. When the direction actually combines or borrows catalog entries, also project the exact, comma-separated `mode_references` / `visual_style_references`; omit the field for a genuinely novel direction and never fabricate a nearby reference. Preserve the confirmed direction, reference locked role names such as `colors.primary` when needed, and omit selection history, contradictions, precedence explanations, or other Design Spec provenance. Executor reads these fields from the retained lock and loads every referenced catalog entry once per valid context. - **page_rhythm is mandatory**: Based on the page list in §IX Content Outline, assign each page one of `anchor` / `dense` / `breathing`. This is what breaks the uniform "every page is a card grid" feel. New locks may not omit the section; consumer omission behavior is owned by [`executor-base.md`](executor-base.md) §2.1. - **Fact IDs and scenario labels are mandatory when applicable**: Read any `sources/*.facts.json`. For each §IX page, list the stable IDs actually used; never cite an ID whose claim is absent from the page. Mark invented KPIs/targets/internal ratios as `Data class: scenario` and state which values are scenario data. Executor carries external sources into notes/footnotes and renders a visible scenario label for scenario figures. - - **Mandatory — whole-roster rhythm check**: During the same §IX composition, compare neighbors and section arcs to judge whether chapter entries visibly reset, extended same-density runs are intentional, extended same-carrier or same-topology runs form an intentional semantic sub-arc, repeated dominant geometry carries a continuity job, each section follows a mode-fitting progression—including framework → explanation/evidence → judgment/action when it serves the objective—and the final arc resolves the communication objective before a genuine ending lowers information load. Repair the existing roster, `Layout`, and `page_rhythm` choices in place. This is judgment, not quota; preserve intentional continuity, legitimately all-`dense` material, and 1:1/literal order. Do not invent filler pages to manufacture rhythm; a `breathing` page marks a meaningful pause—chapter transition, standalone emphasis, or SCQA bridge—and must stand alone. Create no field, lock row, artifact, or second review/execution pass. + - **Mandatory — whole-roster rhythm check**: During the same §IX composition, compare neighbors and section arcs to judge whether chapter entries visibly reset, extended same-density runs are intentional, extended same-carrier or same-topology runs form an intentional semantic sub-arc, repeated dominant geometry carries a continuity job, any qualifying §6.1 visible-state sequence preserves a recognizable mental map while making its next semantic change legible, each section follows a mode-fitting progression—including framework → explanation/evidence → judgment/action when it serves the objective—and the final arc resolves the communication objective before a genuine ending lowers information load. Repair the existing roster, `Layout`, and `page_rhythm` choices in place. This is judgment, not quota; preserve intentional continuity, legitimately all-`dense` material, and 1:1/literal order. Do not invent filler pages to manufacture rhythm; a `breathing` page marks a meaningful pause—chapter transition, standalone emphasis, or SCQA bridge—and must stand alone. Create no field, lock row, artifact, or second review/execution pass. - **Cover impact is mandatory**: In `design_spec.md §IX`, give `P01` one concrete hook from the source's strongest claim, metaphor, number, moment, or conflict plus a recommended composition. The hook binds; Executor may adapt the composition to prepared assets and explicit constraints. With no suitable image, recommend a native-SVG hook instead of a generic title treatment. Beautify / template-fill preservation paths are exempt. - **Cover rhythm lock**: `P01` remains `anchor`. Default away from generic content-page templates; a card grid, agenda, or equal-weight columns remains valid when content, user direction, or the template makes it the clearest cover. - **Closing impact (only when the deck closes)**: For a genuine conclusion / CTA / final takeaway, name the binding takeaway plus a recommended composition; Executor may adapt the latter. Do not default to an information-empty "Thank you", contact-only slide, or cover reprise; an explicit contact/event CTA may serve the purpose. **Do NOT invent a closing page to satisfy this**. Preservation paths are exempt. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md index 8c0ce506..f702454c 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/svg-image-embedding.md @@ -29,12 +29,12 @@ and filter/clip contracts. | Status | Meaning | Executor Handling | |--------|---------|-------------------| -| **Pending** | Acquisition needed (`Acquire Via: ai` / `web`) or derivation needed (`Acquire Via: slice`); not yet attempted | Image Acquisition Phase (Step 5) consumes this; must not remain after Step 5 | +| **Pending** | Acquisition or declared derivation is needed; not yet attempted | Step 5 consumes this; must not remain afterward | | **Failed** | The latest automatic acquisition attempt failed; this is retryable and non-terminal | Step 5 reruns the owning manifest or explicitly resolves the row to `Needs-Manual`; Executor must never treat `Failed` as usable content | -| **Generated** | AI-generated file exists at expected path, or sliced element file exists at expected path | Reference from `../images/`; no on-slide credit needed. **Exception**: an `Illustration Sheet` row is only a slice source — it lives in §VIII but never in `spec_lock.md images`, so the Executor never places it | +| **Generated** | AI/slice output exists | Reference from `../images/`; manifest records govern attribution. An `Illustration Sheet` stays in §VIII only as an unplaced slice source | | **Sourced** | Web-sourced file exists at expected path | Reference from `../images/`; check `image_sources.json` for `license_tier` — if `attribution-required`, render an inline credit element on the slide (see [`executor-web-image.md`](./executor-web-image.md) §1 and [`image-searcher.md`](./image-searcher.md) §7 for the attribution contract) | | **Rendered** | Deterministic formula PNG exists at expected path (`Acquire Via: formula`) | Reference from `../images/`; use a legal anchor with `meet` for the complete placement (centered default: `xMidYMid meet`) and do not crop | -| **Needs-Manual** | Automatic acquisition is unavailable/exhausted or the selected path requires manual fulfillment; for `slice`, the parent sheet is unavailable | Default Generate may use a dashed placeholder until its readiness gate. Quick Generate blocks every required row still in this status, even if an unverified candidate file exists; validate a supplied replacement and reconcile it to `Generated`, `Sourced`, or `Rendered` first. For `slice`, supply the parent sheet and rerun `slice_images.py`; do not hand-place individual element files. | +| **Needs-Manual** | Automatic acquisition is unavailable/exhausted or the selected path requires manual fulfillment; for `slice`, the parent sheet is unavailable | Default Generate may use a dashed placeholder until its readiness gate. Quick Generate blocks every required row still in this status, even if an unverified candidate file exists; validate a supplied replacement and reconcile it to `Existing`, `Generated`, `Sourced`, or `Rendered` first. For `slice`, supply the parent sheet and rerun `slice_images.py`; do not hand-place individual element files. | | **Existing** | User already has image (`Acquire Via: user`) | Place in `images/`, reference with `` | | **Placeholder** | Intentionally not prepared yet (`Acquire Via: placeholder`) | Dashed border placeholder; replace later | @@ -49,6 +49,7 @@ and filter/clip contracts. 2. Prepare project-local resources before SVG authoring: - user → materialize the explicit source under project/images/ → Existing - formula → write formula_manifest.json and run latex_render.py → Rendered + - Pending prepared derivative → follow [`image-base.md`](./image-base.md) §3 before ordinary `Acquire Via` dispatch - Pending / Failed + ai → Image_Generator runs image_gen.py → Generated - Pending / Failed + web → Image_Searcher runs image_search.py → Sourced - Pending + slice → after parent AI sheet is Generated, slice_images.py cuts element files → Generated diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/video-design.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/video-design.md index 33b8a144..1edd89df 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/references/video-design.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/references/video-design.md @@ -52,27 +52,26 @@ SVG roster exists. ## 2. Scene and Page Planning -Judge quality by reduced audience understanding cost, not spectacle. Resolve the -spoken argument, visible evidence, mental map, and motion job together before -choosing effects. +**Default — quality follows purpose (may override)**: explanation prioritizes +understanding; promotion or brand work may prioritize emotion, recall, or +impact. Give every change a communication job. | Narrative relationship | Page treatment | |---|---| | Several lines explain one idea | Keep one page/scene and reveal only the semantic units needed for that explanation | -| One process or system persists | Retain its main roles and relative positions as a stable visual anchor | -| New evidence expands one part of a known map | Keep the map, enlarge or emphasize the active region, and de-emphasize context only as needed | -| The same object changes position, scale, containment, or state | Author compatible adjacent endpoints and use the Morph contract when motion is active | +| One system persists | Before roster/notes freeze, derive states from the prior composition; keep orienting cues and change the semantic delta | +| New evidence expands a known map | Retain orienting cues; adapt the active region and context as needed | +| The same object changes position, scale, containment, or state | Consider compatible Morph endpoints when movement improves orientation | | The audience must adopt a genuinely new mental map | Start a new composition and make the transition explicit | -**Default — stable visual anchors (may override when the subject resets)**: -within one explanation, keep recurring roles, relative positions, and governing -structure stable. Prefer moving, enlarging, revealing, or dimming within that -map over replacing the entire layout at every beat. +**Default — stable visual anchors (may override when the mental map resets)**: +within one explanation, preserve recognizable roles, relationships, or spatial +cues. Position, scale, and style may change while identity and orientation +remain legible; reset for a new map. **Default — one semantic focus change per beat (may override for one inseparable -idea)**: several elements may change together only when they communicate one -unit. Do not change unrelated title, diagram, annotation, and footer regions at -the same moment merely to make the frame busier. +idea)**: change several elements together only for one communication unit. Do +not alter unrelated regions merely for busyness. **Default — scene chrome earns its place (may override for navigation, identity, attribution, or fidelity)**: for newly authored recorded, self-running, or video @@ -88,9 +87,10 @@ on-screen copy)**: place keywords, structure, evidence, and relationships on the slide; keep full explanation in notes. Do not duplicate the narration script as body copy. -**Page-count rule**: derive page count from semantic scenes, visual-state -endpoints, and target duration. Never derive it from subtitle-cue or sentence -count. +**Page-count rule**: derive page/notes boundaries from scenes, mental-map arcs, +endpoints, and duration—not cues or sentences. Profile-fixed count/order/content, +including 1:1/fidelity, permits only existing-neighbor evaluation; never alter +those invariants for motion. --- @@ -120,14 +120,22 @@ A pre-SVG `notes/total.md` is an enabled production artifact, not a forbidden planning checkpoint; Quick still creates no root Design Spec, lock, confirmation payload, or storyboard. +**Hard rule — Quick video Custom Animations**: when Quick generates a PPTX for +recorded, self-running, or video delivery, enable Custom Animations before SVG +authoring and complete the custom-animation stage before base export. Use +semantic groups and page-specific choreography; deck-wide `-a auto` and page +transitions do not satisfy this requirement. Individual pages or groups may +remain static, so this is not an animation-coverage quota. A validated +`animations.json` is required unless the user explicitly requests static or +page-transition-only playback. + **Mandatory — Quick direct video input**: when Quick must deliver a narrated video or MP4 rather than only a deck for later recording, enable Speaker Notes, Narration Audio, and video export; write the complete per-scene narration to `notes/total.md` before P01 and use it as page-design input. After the SVG roster, only agent-authored wording may be finalized; final/literal input remains -verbatim. Before audio, choose static/page-transition-only, -narration-independent deck-wide exporter motion, or page/object-specific Custom -Animations; for the last, decide whether narration governs any group timing. +verbatim. Before audio, complete the required Custom Animations configuration +and decide whether narration governs any group timing. **Production outcomes**: @@ -136,13 +144,16 @@ Animations; for the last, decide whether narration governs any group timing. | Spoken delivery or a supplied final script | Enable Speaker Notes | | User asks the workflow to synthesize narration | Enable Narration Audio; Speaker Notes is its dependency | | Progressive reveal, continuing geometry, or timed emphasis materially aids explanation | Enable/load the appropriate animation capability | -| Quick directly delivers a narrated video or MP4 | Enable Speaker Notes, Narration Audio, and video export; resolve motion before audio, requiring timestamped page-local SRT only for narration-cue sync or subtitle delivery | -| Video is manually narrated or static playback is sufficient | Keep Narration Audio and/or object animation off as applicable | +| Quick generates a PPTX for recorded, self-running, or video delivery | Enable Custom Animations before SVG authoring and validate `animations.json` before base export | +| Quick directly delivers a narrated video or MP4 | Also enable Speaker Notes, Narration Audio, and video export; resolve narration-governed timing before audio, requiring timestamped page-local SRT for cue sync or subtitle delivery | +| The user explicitly requests static playback or disables object motion | Keep object animation off; retain the remaining notes/audio/video outcomes as requested | -**Capability boundary**: a deck intended for later video use does not force -object animation or generated audio. Direct Quick narrated-video delivery uses -the mandatory row above but may intentionally remain static or -page-transition-only. Explicit user instructions remain authoritative. +**Capability boundary**: Default generation does not force object animation or +generated audio merely because a deck may later be recorded. Quick with an +effective recorded/self-running/video delivery purpose does require Custom +Animations, while explicit user instructions for static or +page-transition-only playback remain authoritative. This requirement selects +the capability, not motion coverage or one effect for every page. --- @@ -178,13 +189,18 @@ validate canonical `animations.json`. After the base PPTX/report and timestamped page audio/SRT, map timed groups in `narration_timing.json`, derive `narration_animations.json`, and export the narrated PPTX/MP4. Only derived triggers/delays wait for SRT; object identity, effect, and order do not. `-a -auto` or inherited fixed stagger is not semantic synchronization. For -static/page-transition-only or narration-independent deck-wide motion, omit -these sidecars and the object-sync claim. +auto` or inherited fixed stagger is not semantic synchronization. For an +explicit user-selected static/page-transition-only Quick exception, or for +ordinary Default narration-independent deck-wide motion, omit these sidecars +and the object-sync claim. -**Sound effects**: add them only on explicit request and only from prepared, -project-local assets. Do not introduce sound search, trimming, or normalization -as an implicit video step. +**Sound effects**: exclude them from this pass and planning artifacts. After +final SVG/motion, animation post-processing owns on-demand selection and native +PPTX configuration; otherwise remain silent. For direct narrated MP4 delivery, +`generate-audio` owns the selected sound-delivery branch: native PowerPoint +encoding plus triggered post-export mix, or an explicitly requested real-time +PowerPoint slideshow capture. Video gain and limiting never enter +`animations.json`; capture uses the balance actually heard during Slide Show. **Production sequence**: after the final SVG check, validate any pre-SVG narration against the visible pages; ordinary draft-source runs instead use the @@ -199,14 +215,30 @@ object-sync claim before the narrated PPTX and MP4. ## 5. Delivery Boundary -**Canonical artifact**: the editable PPTX remains canonical. `generate-audio` owns provider/voice/rate -selection, page audio/SRT generation, semantic narration timing, narrated PPTX -export, and optional native PowerPoint video export. +**Canonical artifact**: the editable PPTX remains canonical. `generate-audio` +owns provider/voice/rate selection, page audio/SRT generation, semantic +narration timing, narrated PPTX export, optional native PowerPoint video export, +the explicit slideshow-capture handoff, and the triggered sound-effects mix for +direct MP4 delivery. -**Conditional MP4**: run `powerpoint_video.py --check` only when MP4 delivery is -selected. If native Windows PowerPoint export is unavailable, keep the narrated -PPTX as the successful upstream artifact; do not substitute screenshots, HTML, -or a third-party renderer and call it equivalent. +**Conditional MP4**: run `powerpoint_video.py --check` only for the native-export +branch. If native Windows PowerPoint export is unavailable, keep the narrated +PPTX as the successful upstream artifact. An explicit slideshow-capture choice +may hand that artifact to a user-operated Windows PowerPoint recorder; it is not +complete until the capture is returned and accepted. Do not substitute +screenshots, HTML, or a third-party renderer and call it equivalent. + +**Hard rule — choose one PowerPoint video sound boundary**: + +| Delivery branch | Sound contract | +|---|---| +| Native encoder | PowerPoint supplies visual animation and narration but may omit transition/object sounds. With resolved cues, treat its MP4 as raw and require the verified `video_sound_mix.py` output. | +| Real-time slideshow capture | PowerPoint remains the renderer and audio player; a recorder captures the full-screen Slide Show and exactly one application/system-audio source. The accepted capture must contain narration and every configured cue once, and must not enter `video_sound_mix.py`. | + +The branches are mutually exclusive because mixing a capture would duplicate +its cues. Keep the native cue configuration in the canonical PPTX. Slideshow +capture is explicit and human-audited; it does not inherit the native mix +receipt or become an automatic fallback. **Current boundary**: importing and automatically splitting one long finished recording is unsupported. Require page-level audio or an explicit page/time map; diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md index 89946e1e..253ca2aa 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/README.md @@ -55,8 +55,9 @@ python3 scripts/update_repo.py | SVG pipeline | `preset_shape_svg.py`, `shape_boolean_svg.py`, `svg_authoring_view.py`, `compact_svg_coordinates.py`, `mirror_template_materialize.py`, `finalize_svg.py`, `svg_to_pptx.py`, `template_preview_pptx.py`, `total_md_split.py`, `svg_quality_checker.py`, `extract_svg_assets.py`, `extract_svg_pictures.py`, `animation_config.py`, `notes_to_audio.py`, `narration_sync.py` | [docs/svg-pipeline.md](./docs/svg-pipeline.md); [native shape authoring](../references/native-shape-authoring.md) | | PPTX transitions | `pptx_transitions.py` | [docs/pptx-transitions.md](./docs/pptx-transitions.md) | | PPTX animations | `pptx_animations.py`, `animation_config.py` | [docs/pptx-animations.md](./docs/pptx-animations.md) | +| Animation resources | `sound_sync.py` | [sound catalog](../templates/sounds/README.md); [docs/pptx-animations.md](./docs/pptx-animations.md) | | Spec maintenance | `update_spec.py`, `visualization_recall.py`; legacy `chart_recall.py` | [docs/update_spec.md](./docs/update_spec.md); [docs/visualization-recall.md](./docs/visualization-recall.md) | -| Image tools | `image_gen.py`, `latex_render.py`, `analyze_images.py`, `gemini_watermark_remover.py` | [docs/image.md](./docs/image.md) | +| Image tools | `image_gen.py`, `image_treat.py`, `latex_render.py`, `analyze_images.py`, `gemini_watermark_remover.py` | [docs/image.md](./docs/image.md) | | Maintenance smokes | Inline temporary-project commands | [advanced image and motion](./docs/advanced-image-motion-smoke.md); [mask and gradient](./docs/mask-gradient-smoke.md); [multilingual text](./docs/multilingual-text-smoke.md) | | Repo maintenance | `update_repo.py` | README install/update section | | Troubleshooting | validation, preview, export, dependency issues | [docs/troubleshooting.md](./docs/troubleshooting.md) | diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md index 9e3c6aaa..7162831d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/image.md @@ -163,6 +163,39 @@ MINIMAX_API_KEY=your-api-key # MINIMAX_MODEL=image-01 ``` +## `image_treat.py` + +Create a non-destructive PNG derivative from one bitmap already prepared under +`/images/`. Use this only when a slide needs a baked bitmap effect; +crop, mask, rotation, mirror, opacity, shadow, scrim, outline, and overlap remain +native SVG/PPT treatments. This tool does not perform semantic background +removal: use `slice_images.py --alpha` for flat-color keys, an already prepared +RGBA asset or the active host image editor for a standalone cutout, and +[`image-generator.md`](../../references/image-generator.md) §4.4 only for +registered subject/base layers. + +```bash +python3 scripts/image_treat.py projects/demo hero.jpg \ + --output hero_soft.png --brightness 0.9 --contrast 1.1 --blur 12 + +python3 scripts/image_treat.py projects/demo hero.jpg \ + --output hero_duotone.png --duotone "#14213D" "#FCA311" +``` + +Supported operations are brightness, contrast, desaturation/grayscale, +duotone, and Gaussian blur. They compose in a fixed order: brightness → +contrast → tone treatment → blur. Desaturation, grayscale, and duotone are +mutually exclusive. At least one option must produce a real change; animated +or multi-frame sources are rejected rather than reduced to one frame. + +Both input and output are bare filenames directly under `images/`; output must +be a new `.png` file. The tool keeps the EXIF-corrected display dimensions, +leaves any alpha mask unchanged, and never overwrites the source or an existing +derivative. If `images/image_sources.json` contains the source filename, the +new record inherits that legal provenance and records `derived_from` plus the +ordered `treatments`. Run `analyze_images.py` after all planned derivatives are +ready so the inventory reflects the files that SVG authoring will consume. + ## `analyze_images.py` Analyze objective image-file facts in a project directory before writing the diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md index 8a396400..82e33ad1 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-animations.md @@ -44,7 +44,7 @@ One resolved row contains these fields: | Order | Positive integer sidecar order; ties retain stable SVG group order, then `effects[]` index | | Effect options | Effect-specific `direction`, `amount`, `color`, `font_name` (one installed PowerPoint face, required for Change Font; not a CSS list), `relative`, or `size` values from PowerPoint `EffectParameters` | | Timing options | Repeat count/span, auto-reverse, rewind, accelerate/decelerate, bounce-end ratio, and restart policy | -| Completion | Optional dim/hide behavior and packaged `.m4a`/`.mp3`/`.wav` sound | +| Completion / cue | Optional dim/hide behavior and packaged `.m4a`/`.mp3`/`.wav` sound | Modes resolve before XML writing: @@ -62,6 +62,15 @@ can be audited without replaying the resolver. `animation_config.py scaffold` is neutral: object defaults are `none`, and empty `{}` group placeholders inherit no motion until populated. +Bundled sound discovery/materialization is a workflow concern, not part of the +animation core. After the SVG and object-motion solution are complete, +`sound_sync.py` may copy only selected namespaced ids into +`/sounds/`; new sidecars then reference those project-relative WAV +paths. Existing low-level project-relative/absolute `.m4a`, `.mp3`, and `.wav` +inputs remain compatible. The core never resolves a library id or reads +`templates/sounds/` directly; see [`animations.md`](../../references/animations.md) +§2.2. + --- ## 3. Canonical Registry and Compatibility Inputs diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-transitions.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-transitions.md index 89c278a4..9756633e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-transitions.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/pptx-transitions.md @@ -25,6 +25,7 @@ must not build, replace, or patch a transition with route-local XML or regex. | Layer | Meaning | OOXML | |---|---|---| | Enter | How the current slide appears from the preceding slide | Native effect, Effect Options, and duration | +| Sound | Optional cue played with the current slide's transition | `p:sndAc/p:stSnd/p:snd` with an embedded WAV relationship | | Advance | How the current slide leaves for the next slide | advClick and advTm | Enter policy: @@ -45,8 +46,9 @@ Advance mode: | both | Click or timed advance, whichever occurs first | | narration | Timed advance from narration lead-in, audio duration, and page-tail padding; click disabled | -**Hard rule**: enter=none may coexist with a timed advance. The valid result is -a timing-only p:transition with no visual-effect child. +**Hard rule**: enter=none may coexist with a sound and/or timed advance. The +valid result is a non-visual `p:transition` with `p:sndAc`, advance attributes, +or both, and no visual-effect child. --- @@ -177,11 +179,19 @@ Example: "direction": "left", "pages": "double" }, - "duration": 0.6 + "duration": 0.6, + "sound": "sounds/bigsoundbank/.wav" } } ~~~ +`transition.sound` is optional and accepts a `.wav` path resolved by the +generated-project adapter. A bundled-library choice is synced on demand only +after the visual transition plan is complete, then referenced by its +project-relative `sounds//.wav` path. The adapter packages the +file and passes the shared core an embedded relationship id/name; the core +never reads `templates/sounds/` or resolves library ids. + Inspect the exact contract, including compatibility desugaring: ~~~bash @@ -232,7 +242,7 @@ continue to preserve existing object names and transition XML. | Route | Default enter | Default advance | Compatibility note | |---|---|---|---| -| Generated PPTX CLI | fade, 0.4s | click | auto-advance maps to both | +| Generated PPTX CLI | fade, 0.4s; no sound | click | auto-advance maps to both; an optional sidecar sound is project-local | | Recorded narration | Preserve resolved enter | narration | none remains visually none | | Template Fill | preserve source | preserve source | explicit effects replace; legacy advance_after maps to both | | Native Enhance | Confirmed global/per-slide plan effect | Confirmed timing module | With audio off, an enabled global transition or explicit global `none` applies to all pages; with audio on, the scope flag controls non-narrated pages | @@ -269,7 +279,14 @@ Mutation rules: | preserve | Leave unchanged | Leave wrapper and branches unchanged | | advance-only | Patch direct attributes | Patch Choice and Fallback identically | | replace | Replace the direct carrier | Remove the whole wrapper, then write one carrier | -| none | Remove visual carrier; retain timing-only carrier when needed | Remove the whole wrapper; retain timing-only carrier when needed | +| none | Remove visual effect; retain a non-visual carrier when sound or timing is needed | Remove the whole wrapper; write a non-visual carrier when sound or timing is needed | + +**Sound placement**: for a direct transition, append `p:sndAc` inside +`p:transition` after the visual-effect child, when present. For +`mc:AlternateContent`, write the same sound action into both Choice and +Fallback transition carriers so older Office consumers do not lose the cue. +The slide relationship targets one packaged WAV part; replacing or removing a +requested generated sound must not leave a dangling relationship. **MCE prefix rule**: Requires and Ignorable values contain textual prefix names. Serialization must retain bindings for those exact names. Renaming an @@ -290,6 +307,8 @@ Reject: - unknown option fields or values for the selected effect; - non-finite values, including NaN and Infinity; - duration less than or equal to zero; +- a missing/non-file transition sound, a non-WAV transition sound, or an + unresolved/dangling sound relationship; - negative advance or narration padding; - booleans passed as numeric API values; - multiple logical transition carriers; @@ -300,8 +319,8 @@ Reject: Read-back must report the canonical native effect and complete effective options, while keeping the primary Choice child separate from the fallback. It must also report raw effect attributes, carrier type, duration, click mode, and -automatic advance time. Package validation must run after writing, not only -before mutation. +automatic advance time, plus the transition sound relationship/name when +present. Package validation must run after writing, not only before mutation. Use inline smoke commands and gitignored projects/_smoke_* artifacts. Do not add a tests directory or test_*.py files. diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md index d12b9cdd..10ba0cf3 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/docs/svg-pipeline.md @@ -648,8 +648,8 @@ Behavior: - Narration text is read strictly from the matching `notes/*.md` file; the script only skips Markdown heading lines (`# ...`) and does not summarize, rewrite, or filter delivery notes - `--recorded-narration audio` prepares PowerPoint's "recorded timings and narrations": every slide must have matching `m4a` / `mp3` / `wav` audio, `ffprobe` must read every duration, and `--animation-trigger on-click` is rejected - `--recorded-narration audio` keeps speaker notes, embeds each matching audio file, and writes slide auto-advance timings from page-start lead-in + audio duration + page-tail padding. `--narration-start-floor` and `--narration-padding` are independent optional seconds; their defaults are `0.8` and `0.5`, and the post-transition lead-in is `max(0, start floor - transition duration)` - - When either animation sidecar exists, narrated export defaults to `/narration_animations.json`; a canonical `animations.json` without that derived file remains a blocking synchronization error - - Without animation sidecars, Generate narration reads base-report deck motion via `--inherit-motion-from`; direct low-level omission keeps legacy `fade` / no object builds. Use `--animation-config animations.json` for canonical animation, or `--no-animations` to remove object/page motion while retaining narration timings + - While motion remains enabled, narrated export without an explicit `--animation-config` selects `/narration_animations.json` when either animation sidecar exists; canonical-only cue synchronization therefore blocks until the derived file exists. Narration-independent custom motion explicitly passes `--animation-config animations.json`, even when a derived sidecar also exists + - Without animation sidecars, Generate narration may inherit base-report deck motion via `--inherit-motion-from`; direct low-level omission keeps legacy `fade` / no object builds. Use `--no-animations` to remove object/page motion while retaining narration timings - Non-narrated export keeps the existing optional `/animations.json` default - Narration timing merges into the existing slide timing DOM. While motion remains enabled, object-animation rows and the resolved page transition are preserved rather than regenerated; inherited `-a none` suppresses object rows, and `--no-animations` removes both motion layers - `--narration-audio-dir audio` is the lower-level embedding path: it embeds whatever files match and allows partial audio coverage @@ -675,6 +675,7 @@ Behavior: supplies the default gap between successive non-trigger-shape rows in `after-previous` mode (default `0.5`) - Optional object-level overrides live in `/animations.json` or a path passed via `--animation-config`; build and validate them with `animation_config.py scaffold|validate`. The scaffold is neutral (`defaults.animation.effect: none`, untouched groups `{}`). A populated group uses either the fully compatible legacy single-effect fields or a non-empty `effects[]`, never both; every `effects[]` row names an explicit effect +- Transition/object sound remains off by default. After SVG and visual motion are complete, discover bundled ids with `sound_sync.py list [--query ]` and copy only selected ids with `sound_sync.py / [...]`. `transition.sound` references a project-relative `.wav`; object-animation `sound` accepts the existing `.m4a`/`.mp3`/`.wav` path contract, while bundled selections use the synced project-relative `.wav`. With no selected cue, do not create `/sounds/`. Export never resolves ids or reads `templates/sounds/` directly - One `effects[]` row becomes one Animation Pane record on the group's shape target. Each row may independently set sequence `order`, `delay`, `duration`, `trigger`, and `trigger_shape`; ordinary rows use page-wide order, while `trigger_shape` rows keep relative order in separate interactive sequences and imply `on-click` - Animation configuration is strict: unknown effects/modes/triggers, invalid finite/range/order values, missing slides/groups, and structural-layer targets fail export without fallback or silent omission - Generated export reads every slide back and verifies animation row order, including repeated rows on one shape target, trigger, shape target, resolved effect tuple and native behavior signature, duration, and offset. Package validation then checks timing placement, `p:cTn` ids, and `p:spTgt` references before publication diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_treat.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_treat.py new file mode 100644 index 00000000..e0ba92f6 --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/image_treat.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +""" +PPT Master - Image Treatment + +Create a non-destructive PNG derivative from one project-local bitmap while +preserving its display dimensions, alpha mask, and any matching web provenance. + +Usage: + python3 scripts/image_treat.py --output [options] + +Examples: + python3 scripts/image_treat.py projects/demo hero.jpg --output hero_soft.png --blur 12 + python3 scripts/image_treat.py projects/demo hero.jpg --output hero_duotone.png \ + --contrast 1.1 --duotone "#14213D" "#FCA311" + +Dependencies: + Pillow; project image-search dependencies when copying web provenance + +Treatments run in this fixed order: brightness, contrast, tone treatment +(desaturate / grayscale / duotone), then Gaussian blur. +""" + +from __future__ import annotations + +import argparse +import copy +import math +import os +import re +import sys +import tempfile +from io import BytesIO +from pathlib import Path +from typing import Optional + +from PIL import ( + Image, + ImageCms, + ImageEnhance, + ImageFilter, + ImageOps, + UnidentifiedImageError, +) + + +_SCRIPTS_DIR = Path(__file__).resolve().parent +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + +from console_encoding import configure_utf8_stdio # noqa: E402 + +configure_utf8_stdio() + + +_HEX_COLOR_RE = re.compile(r"^#?([0-9A-Fa-f]{6})$") + + +def _read_sources_manifest(path: Path) -> dict: + from image_search import _read_existing_manifest + + return _read_existing_manifest(path) + + +def _write_sources_manifest(path: Path, item: dict) -> Path: + from image_search import write_sources_manifest + + return write_sources_manifest(path, item) + + +def _finite_float(value: str) -> float: + try: + number = float(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"expected a number, got {value!r}") from exc + if not math.isfinite(number): + raise argparse.ArgumentTypeError("value must be finite") + return number + + +def _nonnegative_float(value: str) -> float: + number = _finite_float(value) + if number < 0: + raise argparse.ArgumentTypeError("value must be greater than or equal to 0") + return number + + +def _positive_float(value: str) -> float: + number = _finite_float(value) + if number <= 0: + raise argparse.ArgumentTypeError("value must be greater than 0") + return number + + +def _unit_float(value: str) -> float: + number = _finite_float(value) + if not 0 <= number <= 1: + raise argparse.ArgumentTypeError("value must be between 0 and 1") + return number + + +def _hex_color(value: str) -> str: + match = _HEX_COLOR_RE.fullmatch(value) + if match is None: + raise argparse.ArgumentTypeError("color must be #RRGGBB or RRGGBB") + return f"#{match.group(1).upper()}" + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Create a project-local PNG derivative. Processing order: brightness -> " + "contrast -> desaturate/grayscale/duotone -> blur." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_path", help="Project root containing an images/ directory.") + parser.add_argument( + "source", + help="Existing bare bitmap filename under /images (no path components).", + ) + parser.add_argument( + "--output", + required=True, + help="New bare .png filename under /images; existing files are never replaced.", + ) + parser.add_argument( + "--brightness", + type=_positive_float, + help="Brightness factor greater than 0; 1 is unchanged.", + ) + parser.add_argument( + "--contrast", + type=_positive_float, + help="Contrast factor greater than 0; 1 is unchanged.", + ) + tone_group = parser.add_mutually_exclusive_group() + tone_group.add_argument( + "--desaturate", + type=_unit_float, + help="Remove this fraction of color (0..1); 1 is grayscale.", + ) + tone_group.add_argument( + "--grayscale", + action="store_true", + help="Convert RGB content to grayscale while preserving any alpha mask.", + ) + tone_group.add_argument( + "--duotone", + nargs=2, + type=_hex_color, + metavar=("SHADOW", "HIGHLIGHT"), + help="Map sRGB luminance between two sRGB #RRGGBB colors.", + ) + parser.add_argument( + "--blur", + type=_nonnegative_float, + help="Gaussian blur radius greater than or equal to 0; 0 is unchanged.", + ) + return parser + + +def _validate_bare_filename(value: str, *, field_name: str) -> str: + if ( + not value.strip() + or value in {".", ".."} + or "/" in value + or "\\" in value + or ":" in value + or Path(value).is_absolute() + ): + raise ValueError( + f"{field_name} must be a bare filename without path components: {value!r}" + ) + return value + + +def _assert_output_absent(output_path: Path) -> None: + for child in output_path.parent.iterdir(): + if child.name.casefold() == output_path.name.casefold(): + raise ValueError(f"output already exists or conflicts by casing: {child}") + + +def _resolve_paths(project_value: str, source_name: str, output_name: str) -> tuple[Path, Path, Path]: + source_name = _validate_bare_filename(source_name, field_name="source") + output_name = _validate_bare_filename(output_name, field_name="output") + if Path(output_name).suffix.casefold() != ".png": + raise ValueError("output must use the .png extension") + if source_name.casefold() == output_name.casefold(): + raise ValueError("output must differ from source, including filename casing") + + project_input = Path(project_value).expanduser() + try: + project_path = project_input.resolve(strict=True) + except (OSError, RuntimeError) as exc: + raise ValueError(f"project path does not resolve safely: {project_input} ({exc})") from exc + if not project_path.is_dir(): + raise ValueError(f"project path is not a directory: {project_path}") + + images_link = project_path / "images" + if images_link.is_symlink(): + raise ValueError(f"project images directory must not be a symlink: {images_link}") + if not images_link.is_dir(): + raise ValueError(f"project images directory does not exist: {images_link}") + images_path = images_link.resolve(strict=True) + if images_path.parent != project_path: + raise ValueError(f"project images directory escapes the project root: {images_link}") + + source_link = images_path / source_name + if source_link.is_symlink(): + raise ValueError(f"source must not be a symlink: {source_link}") + try: + source_path = source_link.resolve(strict=True) + except (OSError, RuntimeError) as exc: + raise ValueError(f"source does not resolve safely: {source_link} ({exc})") from exc + if source_path.parent != images_path or not source_path.is_file(): + raise ValueError(f"source must be a file directly under project images/: {source_link}") + + output_path = images_path / output_name + _assert_output_absent(output_path) + if output_path.exists() or output_path.is_symlink(): + raise ValueError(f"output already exists: {output_path}") + if output_path.parent.resolve() != images_path: + raise ValueError(f"output escapes the project images directory: {output_path}") + + return images_path, source_path, output_path + + +def _treatment_plan(args: argparse.Namespace) -> list[dict]: + plan: list[dict] = [] + if args.brightness is not None and args.brightness != 1: + plan.append({"operation": "brightness", "factor": args.brightness}) + if args.contrast is not None and args.contrast != 1: + plan.append({"operation": "contrast", "factor": args.contrast}) + if args.desaturate is not None and args.desaturate > 0: + plan.append({"operation": "desaturate", "amount": args.desaturate}) + elif args.grayscale: + plan.append({"operation": "grayscale"}) + elif args.duotone is not None: + plan.append( + { + "operation": "duotone", + "shadow": args.duotone[0], + "highlight": args.duotone[1], + } + ) + if args.blur is not None and args.blur > 0: + plan.append({"operation": "blur", "radius": args.blur}) + if not plan: + raise ValueError( + "select at least one effective treatment; identity values such as " + "--brightness 1, --desaturate 0, or --blur 0 do not change the image" + ) + return plan + + +def _validate_blur_radius(radius: Optional[float], width: int, height: int) -> None: + if radius is None or radius <= 0: + return + effective_maximum = max(width, height) + if radius > effective_maximum: + raise ValueError( + f"blur radius {radius:g} exceeds the effective maximum " + f"{effective_maximum} for a {width}x{height} image; choose --blur " + f"at or below {effective_maximum}" + ) + + +def _convert_duotone_source_to_srgb(image: Image.Image) -> tuple[Image.Image, bytes]: + output_profile = ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")) + output_icc = output_profile.tobytes() + source_icc = image.info.get("icc_profile") + if not source_icc: + return image.convert("RGB"), output_icc + if not isinstance(source_icc, bytes): + raise ValueError( + "source ICC profile has an unsupported representation; convert the " + "source to sRGB or remove the invalid profile, then retry" + ) + try: + input_profile = ImageCms.ImageCmsProfile(BytesIO(source_icc)) + converted = ImageCms.profileToProfile( + image, + input_profile, + output_profile, + outputMode="RGB", + ) + except (ImageCms.PyCMSError, OSError, TypeError, ValueError) as exc: + raise ValueError( + "source ICC profile is invalid or incompatible with the image mode; " + "convert the source to sRGB or remove the invalid profile, then retry" + ) from exc + if converted is None: + raise RuntimeError("ICC conversion did not produce an image") + return converted, output_icc + + +def _has_alpha(image: Image.Image) -> bool: + return "A" in image.getbands() or "transparency" in image.info + + +def _apply_treatments( + source_path: Path, + temporary_path: Path, + args: argparse.Namespace, +) -> tuple[int, int]: + try: + with Image.open(source_path) as source: + if int(getattr(source, "n_frames", 1)) != 1: + raise RuntimeError( + f"animated or multi-frame images are unsupported: {source_path}" + ) + oriented = ImageOps.exif_transpose(source) + try: + oriented.load() + width, height = oriented.size + _validate_blur_radius(args.blur, width, height) + has_alpha = _has_alpha(oriented) + rgba = oriented.convert("RGBA") if has_alpha else None + alpha = rgba.getchannel("A") if rgba is not None else None + if args.duotone is not None: + rgb, icc_profile = _convert_duotone_source_to_srgb(oriented) + else: + keep_icc = oriented.mode in {"RGB", "RGBA"} + icc_profile = oriented.info.get("icc_profile") if keep_icc else None + rgb = rgba.convert("RGB") if rgba is not None else oriented.convert("RGB") + + if args.brightness is not None and args.brightness != 1: + rgb = ImageEnhance.Brightness(rgb).enhance(args.brightness) + if args.contrast is not None and args.contrast != 1: + rgb = ImageEnhance.Contrast(rgb).enhance(args.contrast) + if args.desaturate is not None and args.desaturate > 0: + rgb = ImageEnhance.Color(rgb).enhance(1 - args.desaturate) + elif args.grayscale: + rgb = ImageOps.grayscale(rgb).convert("RGB") + elif args.duotone is not None: + rgb = ImageOps.colorize( + ImageOps.grayscale(rgb), + black=args.duotone[0], + white=args.duotone[1], + ) + if args.blur is not None and args.blur > 0: + rgb = rgb.filter(ImageFilter.GaussianBlur(radius=args.blur)) + + result = rgb.convert("RGBA") if alpha is not None else rgb + if alpha is not None: + result.putalpha(alpha) + save_options = {"format": "PNG"} + if isinstance(icc_profile, bytes) and icc_profile: + save_options["icc_profile"] = icc_profile + result.save(temporary_path, **save_options) + result.close() + if rgba is not None: + rgba.close() + if alpha is not None: + alpha.close() + rgb.close() + return width, height + finally: + if oriented is not source: + oriented.close() + except (OSError, UnidentifiedImageError) as exc: + raise RuntimeError(f"unable to read or treat source image {source_path}: {exc}") from exc + + +def _rewrite_attribution_text(item: dict, source_name: str, output_name: str) -> str: + value = item.get("attribution_text") + if not isinstance(value, str) or not value: + return value if isinstance(value, str) else "" + if not value.startswith(source_name): + return value + remainder = value[len(source_name) :] + if remainder and not (remainder[0].isspace() or remainder[0] in "—–-:"): + return value + return output_name + remainder + + +def _prepare_provenance( + manifest_path: Path, + source_name: str, + output_name: str, + treatments: list[dict], +) -> Optional[dict]: + if manifest_path.is_symlink(): + raise ValueError(f"image source manifest must not be a symlink: {manifest_path}") + if not manifest_path.exists(): + return None + + payload = _read_sources_manifest(manifest_path) + items = payload.get("items") or [] + source_item = None + for item in items: + filename = item.get("filename") + if not isinstance(filename, str): + continue + if filename.casefold() == output_name.casefold(): + raise ValueError( + f"output already has an image_sources.json record: {filename!r}" + ) + if filename.casefold() == source_name.casefold(): + if filename != source_name: + raise ValueError( + "source filename casing does not match image_sources.json: " + f"{source_name!r} vs {filename!r}" + ) + source_item = item + + if source_item is None: + return None + derived_item = copy.deepcopy(source_item) + derived_item["filename"] = output_name + derived_item["attribution_text"] = _rewrite_attribution_text( + source_item, + source_name, + output_name, + ) + derived_item["derived_from"] = source_name + derived_item["treatments"] = copy.deepcopy(treatments) + return derived_item + + +def _assert_manifest_output_absent(manifest_path: Path, output_name: str) -> None: + payload = _read_sources_manifest(manifest_path) + for item in payload.get("items") or []: + filename = item.get("filename") + if isinstance(filename, str) and filename.casefold() == output_name.casefold(): + raise RuntimeError( + f"output already has an image_sources.json record: {filename!r}" + ) + + +def _is_staged_output(temporary_path: Path, output_path: Path) -> bool: + try: + return not output_path.is_symlink() and os.path.samefile(temporary_path, output_path) + except OSError: + return False + + +def _write_derivative( + source_path: Path, + output_path: Path, + manifest_path: Path, + provenance_item: Optional[dict], + args: argparse.Namespace, +) -> None: + fd, temporary_name = tempfile.mkstemp( + prefix=f".{output_path.stem}.", + suffix=".tmp.png", + dir=str(output_path.parent), + ) + os.close(fd) + temporary_path = Path(temporary_name) + try: + width, height = _apply_treatments(source_path, temporary_path, args) + if provenance_item is not None: + provenance_item["width"] = width + provenance_item["height"] = height + _assert_manifest_output_absent(manifest_path, output_path.name) + _assert_output_absent(output_path) + try: + os.chmod(temporary_path, 0o644) + except OSError: + pass + try: + os.link(temporary_path, output_path) + except FileExistsError as exc: + raise RuntimeError( + f"output appeared during processing and will not be replaced: {output_path}" + ) from exc + if provenance_item is not None: + try: + if not _is_staged_output(temporary_path, output_path): + raise RuntimeError( + f"installed output changed before provenance commit: {output_path}" + ) + _assert_manifest_output_absent(manifest_path, output_path.name) + _write_sources_manifest(manifest_path, provenance_item) + except Exception as exc: + try: + if _is_staged_output(temporary_path, output_path): + output_path.unlink() + except OSError as cleanup_exc: + raise RuntimeError( + f"manifest update failed and output rollback also failed: {cleanup_exc}" + ) from exc + raise RuntimeError(f"manifest update failed; output was rolled back: {exc}") from exc + finally: + try: + temporary_path.unlink() + except FileNotFoundError: + pass + + +def main(argv: Optional[list[str]] = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + try: + images_path, source_path, output_path = _resolve_paths( + args.project_path, + args.source, + args.output, + ) + treatments = _treatment_plan(args) + manifest_path = images_path / "image_sources.json" + provenance_item = _prepare_provenance( + manifest_path, + args.source, + args.output, + treatments, + ) + _write_derivative( + source_path, + output_path, + manifest_path, + provenance_item, + args, + ) + except (OSError, RuntimeError, ValueError) as exc: + print(f"Error: {exc}", file=sys.stderr) + return 1 + + print(output_path) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py index b32b246e..35bca053 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/narration_sync.py @@ -22,8 +22,8 @@ Examples: --pptx exports/demo.pptx --video exports/demo.mp4 --force Dependencies: - ffprobe for animation-window validation. Optional exported-video calibration - additionally requires ffmpeg and numpy. + ffprobe for animation-window validation. Optional exported-video timeline + calibration additionally requires ffmpeg and numpy. """ from __future__ import annotations @@ -127,6 +127,7 @@ class AnimationGroupState: source_index: int duration_ms: int original_delay_ms: int + trigger: str @dataclass(frozen=True) @@ -174,6 +175,31 @@ class PowerPointTiming: advance_ms: int +@dataclass(frozen=True) +class VideoSlideTiming: + """One slide mapped from the PPTX clock to an exported-video clock.""" + + slide_name: str + transition_ms: int + narration_delay_ms: int + advance_ms: int + powerpoint_slide_start_ms: int + powerpoint_narration_start_ms: int + video_slide_start_ms: int + video_narration_start_ms: int + adjustment_ms: int + correlation: float + audio_path: Path + + +@dataclass(frozen=True) +class VideoTimelineCalibration: + """Page-level calibration between one narrated PPTX and exported video.""" + + slides: tuple[VideoSlideTiming, ...] + powerpoint_timeline_ms: int + + def _timestamp_to_ms(value: str) -> int: hours_text, minutes_text, remainder = value.split(":") seconds_text, milliseconds_text = remainder.split(",") @@ -1010,6 +1036,7 @@ def _resolve_animation_groups( source_index=source_index, duration_ms=playback_duration_ms, original_delay_ms=original_delay_ms, + trigger=effect_trigger, ) ) return states, use_svg @@ -1216,7 +1243,11 @@ def rebuild_animations( f'Derived animation slide "{slide_name}" groups must be an object' ) - previous_end_ms = 0 + # Start modes are row-relative; slide completion spans overlapping rows. + previous_start_ms = 0 + previous_row_end_ms = 0 + timeline_end_ms = 0 + has_previous_row = False referenced_cues: set[int] = set() seen_groups: set[str] = set() for state in states: @@ -1227,20 +1258,24 @@ def rebuild_animations( if first_group_effect else None ) + if not has_previous_row: + sequence_base_ms = 0 + elif state.trigger == "with-previous": + sequence_base_ms = previous_start_ms + else: + sequence_base_ms = previous_row_end_ms if cue_number is None: if first_group_effect: fallback_count += 1 delay_ms = state.original_delay_ms - actual_start_ms = previous_end_ms + delay_ms + actual_start_ms = sequence_base_ms + delay_ms else: anchored_count += 1 referenced_cues.add(cue_number) cue_start_ms = cues[cue_number - 1].start_ms - desired_start_ms = ( - narration_lead_in_ms + cue_start_ms - ) - actual_start_ms = max(desired_start_ms, previous_end_ms) - delay_ms = actual_start_ms - previous_end_ms + desired_start_ms = narration_lead_in_ms + cue_start_ms + actual_start_ms = max(desired_start_ms, sequence_base_ms) + delay_ms = actual_start_ms - sequence_base_ms drift_ms = actual_start_ms - desired_start_ms if drift_ms > 500: drift_warnings.append( @@ -1248,7 +1283,8 @@ def rebuild_animations( f"starts at {_seconds_from_ms(cue_start_ms):.3f}s after " f"a {_seconds_from_ms(narration_lead_in_ms):.3f}s lead-in; " f"animation starts at {_seconds_from_ms(actual_start_ms):.3f}s " - f"(after-previous drift {_seconds_from_ms(drift_ms):.3f}s)" + f"({state.trigger} drift " + f"{_seconds_from_ms(drift_ms):.3f}s)" ) group_value = groups_value.setdefault(state.group_id, {}) @@ -1271,8 +1307,11 @@ def rebuild_animations( effect_value = derived_effect_entries[state.effect_index][1] effect_value["order"] = state.order effect_value["delay"] = _seconds_from_ms(delay_ms) - effect_value["trigger"] = "after-previous" - previous_end_ms = actual_start_ms + state.duration_ms + effect_value["trigger"] = state.trigger + previous_start_ms = actual_start_ms + previous_row_end_ms = actual_start_ms + state.duration_ms + timeline_end_ms = max(timeline_end_ms, previous_row_end_ms) + has_previous_row = True ignored_cue_count += len(cues) - len(referenced_cues) @@ -1284,10 +1323,10 @@ def rebuild_animations( ) * 1000 ) - if previous_end_ms > advance_ms: + if timeline_end_ms > advance_ms: raise ValueError( f'Animations on slide "{slide_name}" end at ' - f"{_seconds_from_ms(previous_end_ms):.3f}s, after the recorded " + f"{_seconds_from_ms(timeline_end_ms):.3f}s, after the recorded " f"slide advance at {_seconds_from_ms(advance_ms):.3f}s" ) @@ -1347,7 +1386,8 @@ def rebuild_animations( ) -def _presentation_slide_members(package: zipfile.ZipFile) -> list[str]: +def presentation_slide_members(package: zipfile.ZipFile) -> list[str]: + """Return slide package members in presentation order.""" try: presentation_root = ET.fromstring(package.read("ppt/presentation.xml")) relationships_root = ET.fromstring( @@ -1400,7 +1440,7 @@ def _read_powerpoint_timings( ) -> list[PowerPointTiming]: timings: list[PowerPointTiming] = [] with zipfile.ZipFile(pptx_path) as package: - slide_members = _presentation_slide_members(package) + slide_members = presentation_slide_members(package) if len(slide_members) != slide_count: raise ValueError( f"Narrated PPTX has {len(slide_members)} slides, " @@ -1467,7 +1507,7 @@ def _require_numpy() -> Any: import numpy as np except ImportError as exc: raise RuntimeError( - "Exported-video subtitle calibration requires numpy. " + "Exported-video timeline calibration requires numpy. " "Install it with: python3 -m pip install numpy" ) from exc return np @@ -1555,11 +1595,15 @@ def _best_correlation(search: Any, template: Any) -> tuple[int, float]: def _alignment_template_bounds( fine_envelope: Any, - cues: list[SubtitleCue], + cues: list[SubtitleCue] | None, ) -> tuple[int, int]: duration_ms = len(fine_envelope) - start_ms = min(cues[0].start_ms, max(0, duration_ms - 500)) - cue_end_ms = min(cues[-1].end_ms, duration_ms) + if cues: + start_ms = min(cues[0].start_ms, max(0, duration_ms - 500)) + cue_end_ms = min(cues[-1].end_ms, duration_ms) + else: + start_ms = 0 + cue_end_ms = duration_ms end_ms = min(duration_ms, start_ms + _ALIGNMENT_TEMPLATE_MAX_MS) end_ms = min(end_ms, max(start_ms + 1000, cue_end_ms)) if end_ms - start_ms < 500: @@ -1572,7 +1616,7 @@ def _locate_audio_start( video_coarse: Any, audio_fine: Any, audio_coarse: Any, - cues: list[SubtitleCue], + cues: list[SubtitleCue] | None, predicted_start_ms: int, ) -> tuple[int, float]: """Locate one page narration near its predicted exported-video position.""" @@ -1624,7 +1668,7 @@ def _locate_audio_start( def _align_audio_starts_to_video( *, slide_names: list[str], - local_cues: dict[str, list[SubtitleCue]], + local_cues: dict[str, list[SubtitleCue] | None], theoretical_starts: list[int], audio_dir: Path, video_path: Path, @@ -1635,7 +1679,7 @@ def _align_audio_starts_to_video( ffmpeg_path = shutil.which("ffmpeg") if ffmpeg_path is None: raise RuntimeError( - "Exported-video subtitle calibration requires ffmpeg. " + "Exported-video timeline calibration requires ffmpeg. " "Install ffmpeg and make it available on PATH." ) @@ -1648,13 +1692,15 @@ def _align_audio_starts_to_video( audio_path = _find_audio(audio_dir, slide_name) audio_paths.append(audio_path) audio_fine, audio_coarse = _decode_audio_envelopes(audio_path, ffmpeg_path) + slide_cues = local_cues.get(slide_name) if ( - local_cues[slide_name][-1].end_ms + slide_cues + and slide_cues[-1].end_ms > len(audio_fine) + _ALIGNMENT_END_TOLERANCE_MS ): raise ValueError( f"{slide_name}.srt ends after its narration audio: " - f"cue end={_seconds_from_ms(local_cues[slide_name][-1].end_ms):.3f}s, " + f"cue end={_seconds_from_ms(slide_cues[-1].end_ms):.3f}s, " f"decoded audio={_seconds_from_ms(len(audio_fine)):.3f}s" ) if index == 0: @@ -1670,7 +1716,7 @@ def _align_audio_starts_to_video( video_coarse, audio_fine, audio_coarse, - local_cues[slide_name], + slide_cues, predicted_start_ms, ) if correlation < _ALIGNMENT_MIN_CORRELATION: @@ -1683,16 +1729,14 @@ def _align_audio_starts_to_video( raise ValueError( f"Exported-video audio order is invalid at slide {slide_name}" ) - final_cue_end_ms = ( - aligned_start_ms + local_cues[slide_name][-1].end_ms - ) + final_audio_end_ms = aligned_start_ms + len(audio_fine) if ( - final_cue_end_ms + final_audio_end_ms > len(video_fine) + _ALIGNMENT_END_TOLERANCE_MS ): raise ValueError( - f"Exported video ends before the final cue on slide {slide_name}: " - f"cue end={_seconds_from_ms(final_cue_end_ms):.3f}s, " + f"Exported video ends before the narration on slide {slide_name}: " + f"audio end={_seconds_from_ms(final_audio_end_ms):.3f}s, " f"decoded video audio={_seconds_from_ms(len(video_fine)):.3f}s" ) aligned_starts.append(aligned_start_ms) @@ -1701,6 +1745,106 @@ def _align_audio_starts_to_video( return aligned_starts, correlations, audio_paths +def calibrate_video_timeline( + *, + slide_names: list[str], + pptx_path: Path, + audio_dir: Path, + video_path: Path, + subtitle_dir: Path | None = None, +) -> VideoTimelineCalibration: + """Calibrate PPTX slide starts against narration in an exported video. + + Page-local SRT improves the correlation template when available. Audio-only + narration remains supported by matching each complete page track. + """ + if not slide_names: + raise ValueError("Video timeline calibration requires at least one slide") + if len(set(slide_names)) != len(slide_names): + raise ValueError("Video timeline calibration slide names must be unique") + + timings = _read_powerpoint_timings(pptx_path, len(slide_names)) + theoretical_starts, timeline_ms = _powerpoint_audio_starts(timings) + local_cues: dict[str, list[SubtitleCue] | None] = {} + for slide_name in slide_names: + subtitle_path = ( + subtitle_dir / f"{slide_name}.srt" + if subtitle_dir is not None + else None + ) + local_cues[slide_name] = ( + _parse_srt(subtitle_path) + if subtitle_path is not None and subtitle_path.is_file() + else None + ) + + aligned_starts, correlations, audio_paths = _align_audio_starts_to_video( + slide_names=slide_names, + local_cues=local_cues, + theoretical_starts=theoretical_starts, + audio_dir=audio_dir, + video_path=video_path, + ) + + slides: list[VideoSlideTiming] = [] + powerpoint_slide_start_ms = 0 + previous_video_slide_start_ms = -1 + for ( + slide_name, + timing, + powerpoint_narration_start_ms, + video_narration_start_ms, + correlation, + audio_path, + ) in zip( + slide_names, + timings, + theoretical_starts, + aligned_starts, + correlations, + audio_paths, + ): + raw_video_slide_start_ms = ( + video_narration_start_ms + - timing.transition_ms + - timing.narration_delay_ms + ) + if raw_video_slide_start_ms < -_ALIGNMENT_END_TOLERANCE_MS: + raise ValueError( + f"Exported-video calibration places slide {slide_name} before " + f"the video start: {raw_video_slide_start_ms}ms" + ) + video_slide_start_ms = max(0, raw_video_slide_start_ms) + if video_slide_start_ms <= previous_video_slide_start_ms: + raise ValueError( + f"Exported-video slide order is invalid at {slide_name}" + ) + slides.append( + VideoSlideTiming( + slide_name=slide_name, + transition_ms=timing.transition_ms, + narration_delay_ms=timing.narration_delay_ms, + advance_ms=timing.advance_ms, + powerpoint_slide_start_ms=powerpoint_slide_start_ms, + powerpoint_narration_start_ms=powerpoint_narration_start_ms, + video_slide_start_ms=video_slide_start_ms, + video_narration_start_ms=video_narration_start_ms, + adjustment_ms=( + video_narration_start_ms - powerpoint_narration_start_ms + ), + correlation=correlation, + audio_path=audio_path, + ) + ) + previous_video_slide_start_ms = video_slide_start_ms + powerpoint_slide_start_ms += timing.transition_ms + timing.advance_ms + + return VideoTimelineCalibration( + slides=tuple(slides), + powerpoint_timeline_ms=timeline_ms, + ) + + def _merge_subtitles_result( project_path: Path, *, @@ -1740,13 +1884,21 @@ def _merge_subtitles_result( audio_starts = theoretical_starts else: resolved_audio_dir = audio_dir or project_path / "audio" - audio_starts, correlations, audio_paths = _align_audio_starts_to_video( + calibration = calibrate_video_timeline( slide_names=slide_names, - local_cues=local_cues, - theoretical_starts=theoretical_starts, + pptx_path=pptx_path, audio_dir=resolved_audio_dir, video_path=video_path, + subtitle_dir=subtitle_dir, ) + if calibration.powerpoint_timeline_ms != timeline_ms: + raise ValueError("Video calibration and subtitle timelines differ") + audio_starts = [ + slide.video_narration_start_ms + for slide in calibration.slides + ] + correlations = [slide.correlation for slide in calibration.slides] + audio_paths = [slide.audio_path for slide in calibration.slides] _reject_output_alias( output_path, audio_paths, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_transitions.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_transitions.py index d96da2b4..22e202eb 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_transitions.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/pptx_transitions.py @@ -44,6 +44,9 @@ P159_NS = "http://schemas.microsoft.com/office/powerpoint/2015/09/main" MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006" PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships" CONTENT_TYPES_NS = "http://schemas.openxmlformats.org/package/2006/content-types" +RELATIONSHIPS_NS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +) PRESENTATION_PROPS_PART = "ppt/presProps.xml" PRESENTATION_RELS_PART = "ppt/_rels/presentation.xml.rels" @@ -54,6 +57,10 @@ PRESENTATION_PROPS_REL_TYPE = ( PRESENTATION_PROPS_CONTENT_TYPE = ( "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" ) +AUDIO_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" +) +WAV_CONTENT_TYPES = frozenset({"audio/wav", "audio/x-wav"}) DEFAULT_TRANSITION = "fade" DEFAULT_TRANSITION_DURATION = 0.4 @@ -882,6 +889,7 @@ TRANSITION_NAMESPACES = { for _prefix, _uri in ( *TRANSITION_NAMESPACES.items(), ("mc", MC_NS), + ("r", RELATIONSHIPS_NS), ): try: ET.register_namespace(_prefix, _uri) @@ -924,6 +932,10 @@ class TransitionSummary: effect_attributes: Mapping[str, str] = field(default_factory=dict) canonical_effect: str | None = None effect_options: Mapping[str, object] = field(default_factory=dict) + sound_relationship_id: str | None = None + sound_name: str | None = None + fallback_sound_relationship_id: str | None = None + fallback_sound_name: str | None = None @dataclass(frozen=True) @@ -1125,6 +1137,7 @@ def describe_transition_effect(effect: object) -> dict[str, Any]: "duration": "positive seconds", "auto_advance": "non-negative seconds", }, + "sound": "project-relative .wav path or null in animations.json", } @@ -1215,18 +1228,67 @@ def _transition_attributes( return " " + " ".join(attrs) if attrs else "" +def _normalize_transition_sound( + sound: Mapping[str, object] | None, +) -> dict[str, str] | None: + """Validate one packaged transition-sound relationship descriptor.""" + if sound is None: + return None + if not isinstance(sound, Mapping): + raise ValueError("transition sound must be a relationship descriptor") + unknown = set(sound) - {"relationship_id", "name"} + if unknown: + raise ValueError( + "transition sound has unknown field(s): " + + ", ".join(sorted(str(field) for field in unknown)) + ) + relationship_id = sound.get("relationship_id") + name = sound.get("name") + if not isinstance(relationship_id, str) or not relationship_id.strip(): + raise ValueError( + "transition sound relationship_id must be a non-empty string" + ) + if not isinstance(name, str) or not name.strip(): + raise ValueError("transition sound name must be a non-empty string") + return { + "relationship_id": relationship_id, + "name": name, + } + + +def _transition_sound_xml( + sound: Mapping[str, str] | None, + *, + indent: str, +) -> str: + """Return the optional p:sndAc payload at one indentation level.""" + if sound is None: + return "" + relationship_id = quoteattr(sound["relationship_id"]) + name = quoteattr(sound["name"]) + return ( + f"{indent}\n" + f"{indent} \n" + f"{indent} \n" + f"{indent} \n" + f"{indent}\n" + ) + + def create_transition_xml( effect: str | None = DEFAULT_TRANSITION, duration: float = 0.5, advance_after: float | None = None, advance_on_click: bool | None = None, effect_options: Mapping[str, object] | None = None, + sound: Mapping[str, object] | None = None, ) -> str: """Build a direct or MCE-backed p:transition XML fragment.""" normalized_effect, normalized_options = normalize_transition_effect_request( effect, effect_options, ) + normalized_sound = _normalize_transition_sound(sound) duration_ms = None if normalized_effect is not None: duration_ms = _seconds_to_ms( @@ -1251,6 +1313,7 @@ def create_transition_xml( normalized_effect is None and advance_ms is None and advance_on_click is None + and normalized_sound is None ): return "" @@ -1261,7 +1324,13 @@ def create_transition_xml( declare_p14=normalized_effect is not None, ) if normalized_effect is None: - return f" " + if normalized_sound is None: + return f" " + return ( + f" \n" + + _transition_sound_xml(normalized_sound, indent=" ") + + " " + ) prefix, element_name, effect_attrs = _effect_xml( normalized_effect, @@ -1282,24 +1351,36 @@ def create_transition_xml( advance_on_click=advance_on_click, declare_p14=False, ) + choice_sound_xml = _transition_sound_xml( + normalized_sound, + indent=" ", + ) + fallback_sound_xml = _transition_sound_xml( + normalized_sound, + indent=" ", + ) return ( f' \n' f' \n' f" \n" f" <{prefix}:{element_name}{effect_attrs}/>\n" + f"{choice_sound_xml}" " \n" " \n" " \n" f" \n" f" <{fallback_prefix}:{fallback_name}{fallback_attrs}/>\n" + f"{fallback_sound_xml}" " \n" " \n" " " ) + sound_xml = _transition_sound_xml(normalized_sound, indent=" ") return ( f" \n" f" <{prefix}:{element_name}{effect_attrs}/>\n" + f"{sound_xml}" " " ) @@ -1493,6 +1574,45 @@ def _effect_identity( return None, None, {} +def _sound_identity( + transition: Any | None, +) -> tuple[str | None, str | None]: + """Return one embedded transition sound relationship and display name.""" + if transition is None: + return None, None + sound_action = next( + ( + child + for child in list(transition) + if child.tag == _qn(PML_NS, "sndAc") + ), + None, + ) + if sound_action is None: + return None, None + start_sound = next( + ( + child + for child in list(sound_action) + if child.tag == _qn(PML_NS, "stSnd") + ), + None, + ) + if start_sound is None: + return None, None + sound = next( + ( + child + for child in list(start_sound) + if child.tag == _qn(PML_NS, "snd") + ), + None, + ) + if sound is None: + return None, None + return sound.get(_qn(RELATIONSHIPS_NS, "embed")), sound.get("name") + + def _effective_transition_options( effect: str, options: Mapping[str, object] | None = None, @@ -1573,6 +1693,10 @@ def read_slide_transition(slide_root: Any) -> TransitionSummary: fallback_effect, fallback_namespace, _fallback_attributes = _effect_identity( fallback ) + sound_relationship_id, sound_name = _sound_identity(primary) + fallback_sound_relationship_id, fallback_sound_name = _sound_identity( + fallback + ) canonical_effect, effect_options = _identify_native_transition( effect, effect_namespace, @@ -1607,6 +1731,10 @@ def read_slide_transition(slide_root: Any) -> TransitionSummary: else None ), advance_after_ms=advance_after_ms, + sound_relationship_id=sound_relationship_id, + sound_name=sound_name, + fallback_sound_relationship_id=fallback_sound_relationship_id, + fallback_sound_name=fallback_sound_name, ) @@ -1806,6 +1934,10 @@ def _visual_identity(summary: TransitionSummary) -> tuple[Any, ...]: summary.fallback_effect_namespace, summary.duration_ms, summary.speed, + summary.sound_relationship_id, + summary.sound_name, + summary.fallback_sound_relationship_id, + summary.fallback_sound_name, ) @@ -1908,6 +2040,11 @@ def _validate_applied_motion( elif enter.policy == "none": if after.effect is not None or after.fallback_effect is not None: errors.append("none policy retained a visual transition") + if ( + after.sound_relationship_id is not None + or after.fallback_sound_relationship_id is not None + ): + errors.append("none policy retained a transition sound") preserved_click = ( before.advance_on_click @@ -2181,6 +2318,7 @@ def validate_generated_transition_xml( advance_on_click: bool | None, advance_after: object | None, effect_options: Mapping[str, object] | None = None, + sound: Mapping[str, object] | None = None, ) -> TransitionSummary: """Validate a generated transition against its resolved settings.""" data = slide_xml.encode("utf-8") if isinstance(slide_xml, str) else slide_xml @@ -2191,6 +2329,7 @@ def validate_generated_transition_xml( effect, effect_options, ) + normalized_sound = _normalize_transition_sound(sound) expected_click = True if advance_on_click is None else advance_on_click if not isinstance(expected_click, bool): errors.append("transition advance_on_click must be a boolean or None") @@ -2207,6 +2346,7 @@ def validate_generated_transition_xml( normalized_effect is not None or expected_after_ms is not None or expected_click is False + or normalized_sound is not None ) if not expects_carrier: @@ -2228,6 +2368,18 @@ def validate_generated_transition_xml( expected_fallback = None expected_fallback_namespace = None expected_attrs: dict[str, Any] = {} + expected_sound_relationship_id = ( + normalized_sound["relationship_id"] + if normalized_sound is not None + else None + ) + expected_sound_name = ( + normalized_sound["name"] + if normalized_sound is not None + else None + ) + expected_fallback_sound_relationship_id = None + expected_fallback_sound_name = None if normalized_effect is not None: ( expected_carrier, @@ -2244,6 +2396,11 @@ def validate_generated_transition_xml( normalized_effect, normalized_options, ) + if expected_carrier == "alternate-content": + expected_fallback_sound_relationship_id = ( + expected_sound_relationship_id + ) + expected_fallback_sound_name = expected_sound_name else: expected_effect_options = {} if ( @@ -2258,6 +2415,11 @@ def validate_generated_transition_xml( or summary.duration_ms != expected_duration_ms or summary.advance_on_click != expected_click or summary.advance_after_ms != expected_after_ms + or summary.sound_relationship_id != expected_sound_relationship_id + or summary.sound_name != expected_sound_name + or summary.fallback_sound_relationship_id + != expected_fallback_sound_relationship_id + or summary.fallback_sound_name != expected_fallback_sound_name ): errors.append("generated transition read-back does not match its settings") if normalized_effect is not None: @@ -2284,6 +2446,177 @@ def validate_generated_transition_xml( return summary +def _slide_relationships_part(slide_part: str) -> str: + directory = posixpath.dirname(slide_part) + filename = posixpath.basename(slide_part) + return posixpath.join(directory, "_rels", f"{filename}.rels") + + +def _resolve_slide_relationship_target( + slide_part: str, + target: str, +) -> str | None: + normalized_target = target.replace("\\", "/") + if not normalized_target: + return None + if normalized_target.startswith("/"): + resolved = posixpath.normpath(normalized_target.lstrip("/")) + else: + resolved = posixpath.normpath( + posixpath.join(posixpath.dirname(slide_part), normalized_target) + ) + if resolved == ".." or resolved.startswith("../"): + return None + return resolved + + +def _transition_sound_elements(slide_root: Any) -> list[Any]: + sounds: list[Any] = [] + for carrier in transition_carriers(slide_root): + for transition in _transition_elements(carrier): + for sound_action in list(transition): + if sound_action.tag != _qn(PML_NS, "sndAc"): + continue + for start_sound in list(sound_action): + if start_sound.tag != _qn(PML_NS, "stSnd"): + continue + sounds.extend( + child + for child in list(start_sound) + if child.tag == _qn(PML_NS, "snd") + ) + return sounds + + +def _package_part_content_type( + package: zipfile.ZipFile, + target_part: str, +) -> str | None: + """Resolve one package part's MIME type from defaults or overrides.""" + content_types_root = ET.fromstring(package.read(CONTENT_TYPES_PART)) + normalized_target = "/" + target_part.lstrip("/") + for entry in content_types_root: + if ( + entry.tag == _qn(CONTENT_TYPES_NS, "Override") + and entry.get("PartName") == normalized_target + ): + return entry.get("ContentType") + extension = Path(target_part).suffix.lstrip(".").lower() + for entry in content_types_root: + if ( + entry.tag == _qn(CONTENT_TYPES_NS, "Default") + and str(entry.get("Extension") or "").lower() == extension + ): + return entry.get("ContentType") + return None + + +def _validate_transition_sound_package_parts( + package: zipfile.ZipFile, + package_names: set[str], + slide_part: str, + slide_xml: bytes, +) -> list[str]: + """Validate transition-sound relationships and embedded WAV targets.""" + errors: list[str] = [] + slide_root = ( + LET.fromstring(slide_xml) + if LET is not None + else parse_source_xml(slide_xml) + ) + sounds = _transition_sound_elements(slide_root) + if not sounds: + return errors + + if CONTENT_TYPES_PART not in package_names: + return ["transition sound is missing [Content_Types].xml"] + + relationships_part = _slide_relationships_part(slide_part) + if relationships_part not in package_names: + return [ + f"transition sound is missing slide relationships: " + f"{relationships_part}" + ] + try: + relationships_root = ET.fromstring(package.read(relationships_part)) + except (KeyError, ET.ParseError) as exc: + return [f"unable to read {relationships_part}: {exc}"] + + relationships: dict[str, Any] = {} + duplicate_ids: set[str] = set() + for relationship in relationships_root: + relationship_id = str(relationship.get("Id") or "") + if not relationship_id: + continue + if relationship_id in relationships: + duplicate_ids.add(relationship_id) + relationships[relationship_id] = relationship + if duplicate_ids: + errors.append( + f"{relationships_part} contains duplicate relationship id(s): " + + ", ".join(sorted(duplicate_ids)) + ) + + for sound in sounds: + relationship_id = sound.get(_qn(RELATIONSHIPS_NS, "embed")) + if not relationship_id: + errors.append("p:snd must declare a non-empty r:embed") + continue + relationship = relationships.get(relationship_id) + if relationship is None: + errors.append( + f"p:snd references missing slide relationship: " + f"{relationship_id}" + ) + continue + if relationship.get("Type") != AUDIO_REL_TYPE: + errors.append( + f"transition sound relationship {relationship_id} must use " + "the OOXML audio relationship type" + ) + if relationship.get("TargetMode") == "External": + errors.append( + f"transition sound relationship {relationship_id} must be internal" + ) + continue + target = str(relationship.get("Target") or "") + target_part = _resolve_slide_relationship_target(slide_part, target) + if target_part is None: + errors.append( + f"transition sound relationship {relationship_id} has an " + f"invalid target: {target!r}" + ) + continue + if Path(target_part).suffix.lower() != ".wav": + errors.append( + f"transition sound relationship {relationship_id} must target " + f"a .wav part: {target_part}" + ) + if target_part not in package_names: + errors.append( + f"transition sound relationship {relationship_id} target is " + f"missing: {target_part}" + ) + continue + content_type = _package_part_content_type(package, target_part) + if str(content_type or "").lower() not in WAV_CONTENT_TYPES: + errors.append( + f"transition sound target {target_part} must declare a WAV " + f"content type; found {content_type!r}" + ) + payload = package.read(target_part) + if not ( + len(payload) >= 12 + and payload[:4] in {b"RIFF", b"RF64"} + and payload[8:12] == b"WAVE" + ): + errors.append( + f"transition sound target {target_part} is not RIFF/RF64 WAVE" + ) + + return errors + + def validate_pptx_transition_package( pptx_path: Path, *, @@ -2311,6 +2644,7 @@ def validate_pptx_transition_package( "duplicate package parts: " + ", ".join(duplicate_names) ) + package_names = set(names) slide_names = sorted( name for name in names @@ -2325,6 +2659,20 @@ def validate_pptx_transition_package( summaries[slide_name] = read_slide_transition_xml(slide_xml) except Exception as exc: errors.append(f"{slide_name}: transition read-back failed: {exc}") + try: + sound_errors = _validate_transition_sound_package_parts( + package, + package_names, + slide_name, + slide_xml, + ) + except Exception as exc: + errors.append( + f"{slide_name}: transition sound read-back failed: {exc}" + ) + else: + for problem in sound_errors: + errors.append(f"{slide_name}: {problem}") if require_use_timings: errors.extend(_validate_package_use_timings(package, names)) @@ -2587,6 +2935,39 @@ def validate_slide_transition_structure(slide_root: Any) -> list[str]: errors.append(f"transition carrier must precede p:{tag}") for carrier in carriers: + for transition in _transition_elements(carrier): + sound_actions = [ + child + for child in list(transition) + if child.tag == _qn(PML_NS, "sndAc") + ] + if len(sound_actions) > 1: + errors.append( + "p:transition must contain at most one p:sndAc; " + f"found {len(sound_actions)}" + ) + if sound_actions: + start_sounds = [ + child + for child in list(sound_actions[0]) + if child.tag == _qn(PML_NS, "stSnd") + ] + if len(start_sounds) > 1: + errors.append( + "p:sndAc must contain at most one p:stSnd; " + f"found {len(start_sounds)}" + ) + if start_sounds: + sounds = [ + child + for child in list(start_sounds[0]) + if child.tag == _qn(PML_NS, "snd") + ] + if len(sounds) != 1: + errors.append( + "p:stSnd must contain exactly one p:snd; " + f"found {len(sounds)}" + ) if carrier.tag != _qn(MC_NS, "AlternateContent"): continue choices = [ diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json index 5b493fac..e20e55d4 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/prompt_audit_manifest.json @@ -373,7 +373,7 @@ "files": [ "skills/ppt-master/scripts/docs/visualization-recall.md" ], - "max_tokens": 105000 + "max_tokens": 120000 }, "route.generate.planning-image": { "description": "Generate-PPTX planning context after a non-none image source is proposed or confirmed, including the compact layout math and vocabulary.", @@ -825,7 +825,7 @@ "files": [ "skills/ppt-master/references/video-design.md" ], - "max_tokens": 2500 + "max_tokens": 3250 }, "stage.generate.refine-spec": { "description": "Explicit post-confirmation spec refinement runbook.", @@ -896,7 +896,7 @@ "files": [ "skills/ppt-master/references/animations.md" ], - "max_tokens": 9000 + "max_tokens": 11000 }, "stage.shared.generate-audio": { "description": "Shared narration-audio stage.", @@ -904,7 +904,7 @@ "files": [ "skills/ppt-master/workflows/stages/generate-audio.md" ], - "max_tokens": 7000 + "max_tokens": 8000 }, "governance.failure-recovery": { "description": "Global stop, retry, and resume policy.", @@ -1181,33 +1181,6 @@ ], "reason": "Corporate brand presets share the same consistent icon convention by design." }, - { - "kind": "exact", - "fingerprint": "da38ec6c641e", - "paths": [ - "skills/ppt-master/SPONSORS.md", - "skills/ppt-master/SPONSORS_CN.md" - ], - "reason": "English and Chinese sponsor pages intentionally reuse the same sponsor badge markup." - }, - { - "kind": "exact", - "fingerprint": "fcc902f6644a", - "paths": [ - "skills/ppt-master/SPONSORS.md", - "skills/ppt-master/SPONSORS_CN.md" - ], - "reason": "English and Chinese sponsor pages intentionally reuse the same sponsor badge markup." - }, - { - "kind": "exact", - "fingerprint": "092a8a1f88e9", - "paths": [ - "skills/ppt-master/SPONSORS.md", - "skills/ppt-master/SPONSORS_CN.md" - ], - "reason": "English and Chinese sponsor pages intentionally reuse the same sponsor badge markup." - }, { "kind": "exact", "fingerprint": "6a432b0cc1df", @@ -1217,15 +1190,6 @@ ], "reason": "Sibling Deck and Layout docs keep the shared replication-mode explanation in lockstep." }, - { - "kind": "exact", - "fingerprint": "aa6e1929dd49", - "paths": [ - "skills/ppt-master/SPONSORS.md", - "skills/ppt-master/SPONSORS_CN.md" - ], - "reason": "English and Chinese sponsor pages intentionally reuse the same sponsor badge markup." - }, { "kind": "exact", "fingerprint": "286b55cb84d7", @@ -2085,6 +2049,14 @@ "glob": "skills/ppt-master/templates/icons/THIRD_PARTY_NOTICES.md", "reason": "Third-party icon license and provenance notice; distributed for compliance, never loaded as prompt context." }, + { + "glob": "skills/ppt-master/templates/sounds/README.md", + "reason": "Bundled sound-library usage and metadata reference; not loaded as runtime prompt context." + }, + { + "glob": "skills/ppt-master/templates/sounds/THIRD_PARTY_NOTICES.md", + "reason": "Third-party sound license and provenance notice; distributed for compliance, never loaded as prompt context." + }, { "glob": "skills/ppt-master/references/shared-standards.md", "reason": "Compatibility router only; runtime load sets select the core and triggered modules directly." diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/sound_sync.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/sound_sync.py new file mode 100644 index 00000000..8aa7b0db --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/sound_sync.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +PPT Master - Sound Sync + +List the bundled CC0 sound catalog or copy explicitly selected sounds into a +project-local `sounds/` directory. The global library is never copied in bulk. + +Usage: + python3 scripts/sound_sync.py list [--query TERM] + python3 scripts/sound_sync.py [ ...] + +Examples: + python3 scripts/sound_sync.py list --query whoosh + python3 scripts/sound_sync.py projects/deck bigsoundbank/1797 kenney-interface/click_001 + +Dependencies: + None (standard library only). + +See templates/sounds/README.md. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import sys +from pathlib import Path +from typing import Optional + +from console_encoding import configure_utf8_stdio + +configure_utf8_stdio() + +_GLOBAL_SOUNDS_DIR = Path(__file__).resolve().parent.parent / "templates" / "sounds" +_INDEX_NAME = "sounds_index.json" + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _is_within(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + except ValueError: + return False + return True + + +def _load_sounds(global_dir: Path) -> list[dict[str, object]]: + index_path = global_dir / _INDEX_NAME + try: + payload = json.loads(index_path.read_text(encoding="utf-8")) + except FileNotFoundError as exc: + raise RuntimeError(f"sound index not found: {index_path}") from exc + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError(f"cannot read sound index {index_path}: {exc}") from exc + + if not isinstance(payload, dict): + raise RuntimeError(f"sound index root is not an object: {index_path}") + if payload.get("version") != 1: + raise RuntimeError(f"unsupported sound index version in {index_path}") + + sounds = payload.get("sounds") + if not isinstance(sounds, list): + raise RuntimeError(f"sound index has no sounds list: {index_path}") + if payload.get("asset_count") != len(sounds): + raise RuntimeError(f"sound index count does not match its sounds list: {index_path}") + + seen_ids: set[str] = set() + for item in sounds: + if not isinstance(item, dict): + raise RuntimeError(f"sound index entry is not an object: {index_path}") + sound_id = item.get("id") + relative_file = item.get("file") + sha256 = item.get("sha256") + if not all(isinstance(value, str) and value for value in (sound_id, relative_file, sha256)): + raise RuntimeError(f"sound index entry lacks id/file/sha256: {index_path}") + if sound_id in seen_ids: + raise RuntimeError(f"duplicate sound id in index: {sound_id}") + seen_ids.add(sound_id) + + relative_path = Path(relative_file) + if relative_path.is_absolute() or ".." in relative_path.parts: + raise RuntimeError(f"unsafe sound path in index: {relative_file}") + if relative_path.suffix.lower() != ".wav": + raise RuntimeError(f"sound index entry is not WAV: {relative_file}") + if relative_path.with_suffix("").as_posix() != sound_id: + raise RuntimeError(f"sound id/file mismatch in index: {sound_id} != {relative_file}") + + return sounds + + +def list_sounds(query: str = "", global_dir: Path = _GLOBAL_SOUNDS_DIR) -> list[dict[str, object]]: + """Return sounds whose metadata contains every case-insensitive query term.""" + sounds = _load_sounds(global_dir) + terms = [term for term in query.casefold().split() if term] + if not terms: + return sounds + + matches: list[dict[str, object]] = [] + for item in sounds: + searchable = [ + str(item.get("id", "")), + str(item.get("label", "")), + str(item.get("source", "")), + *(str(tag) for tag in item.get("tags", [])), + *(str(context) for context in item.get("contexts", [])), + ] + if item.get("recommended") is True: + searchable.append("recommended") + haystack = " ".join(searchable).casefold() + if all(term in haystack for term in terms): + matches.append(item) + return matches + + +def sync_sounds( + project_path: Path, + sound_ids: list[str], + global_dir: Path = _GLOBAL_SOUNDS_DIR, +) -> tuple[list[str], list[str]]: + """Copy selected sound IDs into `/sounds/` after validating the full batch. + + Returns `(copied_or_present, missing)`. Any missing ID is reported before + creating the project-local directory or copying a file. + """ + try: + project_root = project_path.resolve(strict=True) + except OSError as exc: + raise RuntimeError(f"cannot resolve project directory {project_path}: {exc}") from exc + if not project_root.is_dir(): + raise RuntimeError(f"project is not a directory: {project_path}") + + sounds = _load_sounds(global_dir) + catalog = {str(item["id"]): item for item in sounds} + requested = list(dict.fromkeys(sound_ids)) + missing = [sound_id for sound_id in requested if sound_id not in catalog] + if missing: + return [], missing + + validated: list[tuple[str, Path, Path, bool]] = [] + for sound_id in requested: + item = catalog[sound_id] + relative_path = Path(str(item["file"])) + source = global_dir / relative_path + expected_sha256 = str(item["sha256"]) + if not source.is_file(): + raise RuntimeError(f"library file not found for {sound_id}: {source}") + actual_sha256 = _sha256(source) + if actual_sha256 != expected_sha256: + raise RuntimeError( + f"library checksum mismatch for {sound_id}: " + f"expected {expected_sha256}, got {actual_sha256}" + ) + destination = project_root / "sounds" / relative_path + resolved_parent = destination.parent.resolve(strict=False) + resolved_destination = destination.resolve(strict=False) + if not _is_within(resolved_parent, project_root): + raise RuntimeError( + f"destination parent escapes project root for {sound_id}: {resolved_parent}" + ) + if not _is_within(resolved_destination, project_root): + raise RuntimeError( + f"destination escapes project root for {sound_id}: {resolved_destination}" + ) + if destination.is_symlink(): + raise RuntimeError(f"destination symlink is not allowed for {sound_id}: {destination}") + + already_present = False + if destination.exists(): + if not destination.is_file(): + raise RuntimeError(f"destination is not a regular file for {sound_id}: {destination}") + destination_sha256 = _sha256(destination) + if destination_sha256 != expected_sha256: + raise RuntimeError( + f"project sound conflicts with library id {sound_id}: {destination}; " + "remove or rename the existing file, then rerun" + ) + already_present = True + validated.append((sound_id, source, destination, already_present)) + + copied: list[str] = [] + for sound_id, source, destination, already_present in validated: + if already_present: + copied.append(f"{sound_id} (already in project)") + continue + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + copied.append(sound_id) + + return copied, [] + + +def _build_list_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="sound_sync.py list", + description="List bundled sounds, optionally filtered by metadata.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--query", + default="", + help="Case-insensitive terms matched against ID, label, source, tags, and contexts", + ) + return parser + + +def _build_sync_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Copy explicitly selected library sounds into a project's sounds/ folder. " + "Use `sound_sync.py list [--query TERM]` for discovery." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_path", help="Existing project directory") + parser.add_argument("sound_ids", nargs="+", help="Sound IDs such as bigsoundbank/1797") + return parser + + +def _run_list(argv: list[str]) -> int: + args = _build_list_parser().parse_args(argv) + try: + sounds = list_sounds(args.query) + except RuntimeError as exc: + print(f"[ERROR] {exc}", file=sys.stderr) + return 1 + + for item in sounds: + marker = "*" if item.get("recommended") is True else " " + sound_id = str(item["id"]) + duration = float(item.get("duration_seconds", 0.0)) + label = str(item.get("label", "")) + contexts = ",".join(str(value) for value in item.get("contexts", [])) + print(f"{marker} {sound_id:<42} {duration:>7.3f}s {label} [{contexts}]") + + print(f"[OK] {len(sounds)} sound(s) matched", file=sys.stderr) + return 0 + + +def _run_sync(argv: list[str]) -> int: + args = _build_sync_parser().parse_args(argv) + project = Path(args.project_path) + if not project.is_dir(): + print(f"[ERROR] project not found: {project}", file=sys.stderr) + return 1 + + try: + copied, missing = sync_sounds(project, args.sound_ids) + except RuntimeError as exc: + print(f"[ERROR] {exc}", file=sys.stderr) + return 1 + + if missing: + print( + f"[MISSING] {len(missing)} sound ID(s) not in the library; nothing was copied:", + file=sys.stderr, + ) + for sound_id in missing: + print(f" x {sound_id}", file=sys.stderr) + print( + "Run `python3 skills/ppt-master/scripts/sound_sync.py list --query ` " + "and select a listed ID.", + file=sys.stderr, + ) + return 1 + + print(f"[OK] {len(copied)} sound(s) in {project / 'sounds'}:", file=sys.stderr) + for sound_id in copied: + print(f" + {sound_id}", file=sys.stderr) + return 0 + + +def main(argv: Optional[list[str]] = None) -> int: + raw_args = list(sys.argv[1:] if argv is None else argv) + if raw_args and raw_args[0] == "list": + return _run_list(raw_args[1:]) + return _run_sync(raw_args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py index 3adbb289..2b95c09e 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/animation_config.py @@ -6,7 +6,7 @@ import json import math import re from dataclasses import dataclass -from pathlib import Path +from pathlib import Path, PureWindowsPath from typing import Any from xml.etree import ElementTree as ET @@ -541,7 +541,13 @@ def _transition_scope_errors( errors = _unknown_field_errors( transition, - frozenset({'effect', 'effect_options', 'duration', 'auto_advance'}), + frozenset({ + 'effect', + 'effect_options', + 'duration', + 'auto_advance', + 'sound', + }), f'{label} transition', ) effect = transition.get('effect', inherited_effect) @@ -576,6 +582,24 @@ def _transition_scope_errors( ) except ValueError as exc: errors.append(str(exc)) + if 'sound' in transition: + sound = transition['sound'] + if sound is None: + return errors + if not isinstance(sound, str) or not sound.strip(): + errors.append( + f'animations.json {label} transition sound must be a ' + 'non-empty project-relative .wav path or null' + ) + elif Path(sound).is_absolute() or PureWindowsPath(sound).drive: + errors.append( + f'animations.json {label} transition sound must be ' + f'project-relative: {sound!r}' + ) + elif Path(sound).suffix.lower() != '.wav': + errors.append( + f'animations.json {label} transition sound must use .wav' + ) return errors @@ -1252,6 +1276,77 @@ def _animation_sound_path_errors( return errors +def _declared_transition_sounds( + config: dict[str, Any], +) -> tuple[tuple[str, object], ...]: + """Return explicitly declared non-null transition sound values.""" + sounds: list[tuple[str, object]] = [] + defaults = config.get('defaults', {}) + if isinstance(defaults, dict): + transition = defaults.get('transition', {}) + if ( + isinstance(transition, dict) + and transition.get('sound') is not None + ): + sounds.append(('defaults transition', transition['sound'])) + + slides = config.get('slides', {}) + if not isinstance(slides, dict): + return tuple(sounds) + for slide_name, slide_cfg in slides.items(): + if not isinstance(slide_cfg, dict): + continue + transition = slide_cfg.get('transition', {}) + if ( + isinstance(transition, dict) + and transition.get('sound') is not None + ): + sounds.append( + (f'slide "{slide_name}" transition', transition['sound']) + ) + return tuple(sounds) + + +def _transition_sound_path_errors( + project_path: Path, + config: dict[str, Any], +) -> list[str]: + """Validate transition sounds as project-contained WAV files.""" + errors: list[str] = [] + project_root = project_path.resolve() + for label, raw_sound in _declared_transition_sounds(config): + if not isinstance(raw_sound, str) or not raw_sound.strip(): + continue + sound_path = Path(raw_sound) + if sound_path.is_absolute() or PureWindowsPath(raw_sound).drive: + errors.append( + f'animations.json {label} sound must be project-relative: ' + f'{raw_sound!r}' + ) + continue + if sound_path.suffix.lower() != '.wav': + continue + resolved_path = (project_root / sound_path).resolve() + try: + resolved_path.relative_to(project_root) + except ValueError: + errors.append( + f'animations.json {label} sound escapes the project root: ' + f'{raw_sound!r}' + ) + continue + if not resolved_path.exists(): + errors.append( + f'animations.json {label} sound file not found: {resolved_path}' + ) + elif not resolved_path.is_file(): + errors.append( + f'animations.json {label} sound path is not a regular file: ' + f'{resolved_path}' + ) + return errors + + def validate_animation_config( project_path: Path, config: dict[str, Any] | None = None, @@ -1273,6 +1368,7 @@ def validate_animation_config( return [] warnings = _animation_sound_path_errors(project_path, config) + warnings.extend(_transition_sound_path_errors(project_path, config)) targets_by_slide, anonymous_groups = scan_project_targets( project_path, svg_files=svg_files, @@ -1431,7 +1527,11 @@ def build_scaffold(project_path: Path) -> dict[str, Any]: to remind the editor that deck-wide overrides exist and most pages should inherit them. """ - transition_defaults = {'effect': 'fade', 'duration': 0.4} + transition_defaults = { + 'effect': 'fade', + 'duration': 0.4, + 'sound': None, + } animation_defaults = { 'effect': 'none', 'duration': 0.4, @@ -1448,7 +1548,10 @@ def build_scaffold(project_path: Path) -> dict[str, Any]: continue groups[target.group_id] = {} slides[slide_name] = { - 'transition': dict(transition_defaults), + 'transition': { + 'effect': transition_defaults['effect'], + 'duration': transition_defaults['duration'], + }, 'animation': dict(animation_defaults), 'groups': groups, } diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py index 61dbd01a..e448b507 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/builder.py @@ -19,7 +19,7 @@ import zipfile from concurrent.futures import ProcessPoolExecutor, as_completed from dataclasses import asdict, dataclass from datetime import datetime, timezone -from pathlib import Path +from pathlib import Path, PureWindowsPath from typing import Any from xml.etree import ElementTree as ET from xml.sax.saxutils import escape @@ -4002,13 +4002,15 @@ def _slide_config(animation_config: dict[str, Any] | None, svg_stem: str) -> dic def _slide_transition_settings( + default_transition_cfg: dict[str, Any], slide_cfg: dict[str, Any], transition: str | None, transition_effect_options: dict[str, object] | None, duration: float, auto_advance: float | None, + transition_sound: str | None, cli_overrides: dict[str, bool], -) -> tuple[str | None, dict[str, object], float, float | None]: +) -> tuple[str | None, dict[str, object], float, float | None, str | None]: trans_value = slide_cfg.get('transition', {}) if not isinstance(trans_value, dict): raise ValueError('animations.json slide transition must be an object') @@ -4043,7 +4045,19 @@ def _slide_transition_settings( "transition auto_advance", allow_zero=True, ) - return effect, effect_options, duration, auto_advance + raw_sound = transition_sound + if raw_sound is None and not cli_overrides.get('transition_sound'): + raw_sound = default_transition_cfg.get('sound') + if 'sound' in trans_cfg: + raw_sound = trans_cfg['sound'] + if raw_sound is not None and ( + not isinstance(raw_sound, str) or not raw_sound.strip() + ): + raise ValueError( + 'animations.json transition sound must be a non-empty ' + 'project-relative .wav path or null' + ) + return effect, effect_options, duration, auto_advance, raw_sound def _slide_animation_settings( @@ -4366,63 +4380,134 @@ def _next_relationship_id(rel_entries: list[dict[str, str]]) -> str: return f'rId{candidate}' +def _materialize_slide_sound( + project_path: Path, + raw_sound: str, + media_files: dict[str, bytes], + rel_entries: list[dict[str, str]], + audio_exts_used: set[str], + packaged_by_source: dict[Path, tuple[str, str]], + *, + label: str, + media_prefix: str, + require_project_relative_wav: bool, +) -> dict[str, str]: + """Package one slide sound and return its relationship descriptor.""" + if not isinstance(raw_sound, str) or not raw_sound.strip(): + raise ValueError(f'{label} sound must be a non-empty path string') + sound_path = Path(raw_sound) + if require_project_relative_wav: + if sound_path.is_absolute() or PureWindowsPath(raw_sound).drive: + raise ValueError(f'{label} sound must be project-relative: {raw_sound!r}') + extension = sound_path.suffix.lower() + if extension != '.wav': + raise ValueError(f'{label} sound must use .wav') + project_root = project_path.resolve() + sound_path = (project_root / sound_path).resolve() + try: + sound_path.relative_to(project_root) + except ValueError as exc: + raise ValueError( + f'{label} sound escapes the project root: {raw_sound!r}' + ) from exc + else: + if not sound_path.is_absolute(): + sound_path = project_path / sound_path + sound_path = sound_path.resolve() + extension = sound_path.suffix.lower() + + if not sound_path.is_file(): + raise ValueError(f'{label} sound file not found: {sound_path}') + if extension not in AUDIO_CONTENT_TYPES: + valid = ', '.join(sorted(AUDIO_CONTENT_TYPES)) + raise ValueError( + f'unsupported {label} sound format {extension or "(none)"}; ' + f'valid formats: {valid}' + ) + + packaged = packaged_by_source.get(sound_path) + if packaged is None: + payload = sound_path.read_bytes() + if require_project_relative_wav and not ( + len(payload) >= 12 + and payload[:4] in {b'RIFF', b'RF64'} + and payload[8:12] == b'WAVE' + ): + raise ValueError(f'{label} sound is not a valid WAV file: {sound_path}') + digest = hashlib.sha256(payload).hexdigest()[:16] + media_name = f'{media_prefix}_{digest}{extension}' + relationship_id = _next_relationship_id(rel_entries) + media_files.setdefault(media_name, payload) + rel_entries.append( + { + 'id': relationship_id, + 'type': AUDIO_REL_TYPE, + 'target': f'../media/{media_name}', + } + ) + packaged = (relationship_id, media_name) + packaged_by_source[sound_path] = packaged + audio_exts_used.add(extension) + + relationship_id, _media_name = packaged + return { + 'relationship_id': relationship_id, + 'name': sound_path.name, + } + + +def _materialize_transition_sound( + project_path: Path, + raw_sound: str | None, + media_files: dict[str, bytes], + rel_entries: list[dict[str, str]], + audio_exts_used: set[str], + packaged_by_source: dict[Path, tuple[str, str]], +) -> dict[str, str] | None: + """Package one optional project-local WAV for a slide transition.""" + if raw_sound is None: + return None + return _materialize_slide_sound( + project_path, + raw_sound, + media_files, + rel_entries, + audio_exts_used, + packaged_by_source, + label='transition', + media_prefix='transition_sound', + require_project_relative_wav=True, + ) + + def _materialize_animation_sounds( project_path: Path, targets: list[dict[str, Any]], media_files: dict[str, bytes], rel_entries: list[dict[str, str]], audio_exts_used: set[str], + packaged_by_source: dict[Path, tuple[str, str]] | None = None, ) -> list[dict[str, Any]]: """Package sidecar sound files and replace paths with OOXML relationships.""" materialized: list[dict[str, Any]] = [] - packaged_by_source: dict[Path, tuple[str, str]] = {} + packaged_by_source = packaged_by_source if packaged_by_source is not None else {} for index, raw_target in enumerate(targets, 1): target = dict(raw_target) raw_sound = target.pop('_sound_path', None) if raw_sound is None: materialized.append(target) continue - if not isinstance(raw_sound, str) or not raw_sound.strip(): - raise ValueError( - f'animation target {index} sound must be a non-empty path string' - ) - sound_path = Path(raw_sound) - if not sound_path.is_absolute(): - sound_path = project_path / sound_path - sound_path = sound_path.resolve() - if not sound_path.is_file(): - raise ValueError(f'animation sound file not found: {sound_path}') - extension = sound_path.suffix.lower() - if extension not in AUDIO_CONTENT_TYPES: - valid = ', '.join(sorted(AUDIO_CONTENT_TYPES)) - raise ValueError( - f'unsupported animation sound format {extension or "(none)"}; ' - f'valid formats: {valid}' - ) - - packaged = packaged_by_source.get(sound_path) - if packaged is None: - payload = sound_path.read_bytes() - digest = hashlib.sha256(payload).hexdigest()[:16] - media_name = f'animation_sound_{digest}{extension}' - relationship_id = _next_relationship_id(rel_entries) - media_files.setdefault(media_name, payload) - rel_entries.append( - { - 'id': relationship_id, - 'type': AUDIO_REL_TYPE, - 'target': f'../media/{media_name}', - } - ) - packaged = (relationship_id, media_name) - packaged_by_source[sound_path] = packaged - audio_exts_used.add(extension) - - relationship_id, _media_name = packaged - target['sound'] = { - 'relationship_id': relationship_id, - 'name': sound_path.name, - } + target['sound'] = _materialize_slide_sound( + project_path, + raw_sound, + media_files, + rel_entries, + audio_exts_used, + packaged_by_source, + label=f'animation target {index}', + media_prefix='animation_sound', + require_project_relative_wav=False, + ) materialized.append(target) return materialized @@ -4683,6 +4768,7 @@ def create_pptx_with_native_svg( expected_viewbox: str | None = None, animation_resource_root: Path | None = None, transition_effect_options: dict[str, object] | None = None, + transition_sound: str | None = None, text_flow: str | None = None, primary_language: str | None = None, narration_start_floor: float = DEFAULT_NARRATION_START_FLOOR, @@ -4698,12 +4784,15 @@ def create_pptx_with_native_svg( canvas_format: Canvas format key. expected_viewbox: Optional project/template-lock canvas contract. Every public page and internal Layout definition must match it. - animation_resource_root: Base directory for relative animation sound - paths. Defaults to the parent of the SVG source directory. + animation_resource_root: Project root for sidecar sound paths. Object + animation sounds retain existing absolute-path compatibility; + transition sounds must remain project-relative WAV files. verbose: Whether to output detailed information. transition: Transition effect name. transition_effect_options: PowerPoint Effect Options for the selected native page transition. + transition_sound: Optional project-relative WAV path used by the + generated page transition. transition_duration: Transition duration in seconds. auto_advance: Auto-advance interval in seconds. use_compat_mode: Retained for API compatibility; ignored in native mode. @@ -5085,9 +5174,14 @@ def create_pptx_with_native_svg( audio_exts_used: set[str] = set() package_uses_timings = False mixed_animation_offset = 0 - animation_defaults_value = _as_dict( - _as_dict(animation_config).get('defaults') - ).get('animation', {}) + config_defaults = _as_dict(_as_dict(animation_config).get('defaults')) + transition_defaults_value = config_defaults.get('transition', {}) + if not isinstance(transition_defaults_value, dict): + raise ValueError( + 'animations.json defaults transition must be an object' + ) + default_transition_cfg = transition_defaults_value + animation_defaults_value = config_defaults.get('animation', {}) if not isinstance(animation_defaults_value, dict): raise ValueError( 'animations.json defaults animation must be an object' @@ -5123,6 +5217,7 @@ def create_pptx_with_native_svg( expected_animation_targets: list[dict[str, Any]] = [] expected_animation_duration = animation_duration expected_animation_trigger = normalize_animation_trigger(animation_trigger) + expected_transition_sound: dict[str, str] | None = None try: # ---- Native shapes mode ---- @@ -5137,6 +5232,7 @@ def create_pptx_with_native_svg( slide_transition_effect_options = {} slide_transition_duration = transition_duration slide_auto_advance = None + slide_transition_sound_path = None slide_animation = None slide_animation_duration = animation_duration slide_animation_stagger = animation_stagger @@ -5148,12 +5244,15 @@ def create_pptx_with_native_svg( slide_transition_effect_options, slide_transition_duration, slide_auto_advance, + slide_transition_sound_path, ) = _slide_transition_settings( + default_transition_cfg, slide_cfg, transition, transition_effect_options, transition_duration, auto_advance, + transition_sound, animation_cli_overrides, ) ( @@ -5295,12 +5394,30 @@ def create_pptx_with_native_svg( # to precede inside . Both use the same # string-replace anchor, so transition must be # injected first and timing second. - if slide_transition is not None or slide_auto_advance is not None: + packaged_sounds_by_source: dict[Path, tuple[str, str]] = {} + expected_transition_sound = _materialize_transition_sound( + ( + animation_resource_root + if animation_resource_root is not None + else svg_files[0].parent.parent + ), + slide_transition_sound_path, + media_files_dict, + rel_entries, + audio_exts_used, + packaged_sounds_by_source, + ) + if ( + slide_transition is not None + or slide_auto_advance is not None + or expected_transition_sound is not None + ): transition_fragment = create_transition_xml( effect=slide_transition, duration=slide_transition_duration, advance_after=slide_auto_advance, effect_options=slide_transition_effect_options, + sound=expected_transition_sound, ) if transition_fragment: slide_xml = slide_xml.replace( @@ -5338,6 +5455,7 @@ def create_pptx_with_native_svg( media_files_dict, rel_entries, audio_exts_used, + packaged_sounds_by_source, ) expected_animation_targets = seq_targets if mixed_count: @@ -5447,13 +5565,16 @@ def create_pptx_with_native_svg( slide_transition_effect_options, slide_transition_duration, slide_auto_advance, + slide_transition_sound_path, ) = ( _slide_transition_settings( + default_transition_cfg, slide_cfg, transition, transition_effect_options, transition_duration, auto_advance, + transition_sound, animation_cli_overrides, ) ) @@ -5640,6 +5761,7 @@ def create_pptx_with_native_svg( duration=slide_transition_duration, advance_on_click=resolved_advance_on_click, advance_after=resolved_advance_after, + sound=expected_transition_sound, ) except ValueError as exc: raise RuntimeError( diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py index 0648875b..aaeb7b8b 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/svg_to_pptx/pptx_package/cli.py @@ -875,9 +875,10 @@ Recorded narration: - Keeps speaker notes when enabled - Prepares PowerPoint recorded timings and narrations - Requires one m4a/mp3/wav file per slide - - Uses narration_animations.json when animation sidecars exist - - Inherits source-bound deck motion from the base postflight report - - Use --animation-config animations.json for the canonical animation + - Unless --no-animations is set, recorded narration without an explicit + config selects narration_animations.json when either sidecar exists + - With no sidecar, --inherit-motion-from may restore base-report deck motion + - Use --animation-config animations.json for narration-independent custom motion - Use --no-animations for narration and timings without animation motion - Embeds per-slide audio matched by SVG filename / slide number - Sets slide auto-advance from audio duration so video export can use @@ -1064,9 +1065,11 @@ Recorded narration: type=str, default=None, help=( - 'Per-slide/per-object animation config. Recorded narration uses ' - '/narration_animations.json when an animation sidecar exists, ' - 'or may inherit base postflight motion with --inherit-motion-from. ' + 'Per-slide/per-object animation config. While motion remains enabled, ' + 'recorded narration without an explicit config selects ' + '/narration_animations.json when either animation sidecar ' + 'exists, or may inherit base postflight motion with ' + '--inherit-motion-from when neither exists. ' 'Other exports default to /animations.json when present.' ), ) @@ -1721,6 +1724,22 @@ Recorded narration: "transition auto_advance", allow_zero=True, ) + transition_sound = ( + None + if args.no_animations + else ( + inherited_transition['sound'] + if 'sound' in inherited_transition + else transition_defaults.get('sound') + ) + ) + if transition_sound is not None and not isinstance( + transition_sound, + str, + ): + raise ValueError( + 'transition sound must be a project-relative .wav path or null' + ) except ValueError as exc: print(f"Error: {exc}", file=sys.stderr) return 1 @@ -1813,6 +1832,7 @@ Recorded narration: args.auto_advance is not None or inherited_overrides.get('auto_advance') is True ), + 'transition_sound': 'sound' in inherited_transition, 'animation': ( args.animation is not None or inherited_overrides.get('animation') is True @@ -1837,6 +1857,7 @@ Recorded narration: 'effect_options': transition_effect_options, 'duration': transition_duration, 'auto_advance': auto_advance, + 'sound': transition_sound, }, 'animation': { 'effect': normalized_animation or 'none', @@ -1900,6 +1921,7 @@ Recorded narration: verbose=verbose, transition=transition, transition_effect_options=transition_effect_options, + transition_sound=transition_sound, transition_duration=transition_duration, auto_advance=auto_advance, notes=notes, diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_sound_mix.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_sound_mix.py new file mode 100644 index 00000000..69c401bf --- /dev/null +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_sound_mix.py @@ -0,0 +1,1624 @@ +#!/usr/bin/env python3 +""" +PPT Master - Video Sound Mix + +Recover transition and object-animation sound cues from the final narrated +PPTX, calibrate them to a PowerPoint-exported video, render an independent SFX +stem, and mix that stem with the video's narration track. +See workflows/stages/generate-audio.md for the owning delivery stage. + +Usage: + python3 scripts/video_sound_mix.py --pptx \ + --trace --video [options] + +Examples: + python3 scripts/video_sound_mix.py projects/demo \ + --pptx exports/demo_narrated.pptx \ + --trace validation/demo_narrated.trace.json \ + --video exports/demo_raw.mp4 --force + +Dependencies: + ffmpeg, ffprobe, and numpy +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +import re +import shutil +import subprocess +import sys +import tempfile +import zipfile +from dataclasses import dataclass +from fractions import Fraction +from pathlib import Path +from typing import Any +from xml.etree import ElementTree as ET + +_SCRIPTS_DIR = Path(__file__).resolve().parent +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + +from console_encoding import configure_utf8_stdio # noqa: E402 +from narration_sync import ( # noqa: E402 + VideoTimelineCalibration, + calibrate_video_timeline, + presentation_slide_members, +) +from pptx_animations import read_slide_animation_sequence # noqa: E402 +from pptx_opc_validation import ( # noqa: E402 + canonical_opc_part_path, + resolve_internal_opc_target, +) +from pptx_transitions import ( # noqa: E402 + AUDIO_REL_TYPE, + read_slide_transition_xml, +) + +configure_utf8_stdio() + + +_PACKAGE_REL_NS = ( + "http://schemas.openxmlformats.org/package/2006/relationships" +) +_RELATIONSHIP_TAG = f"{{{_PACKAGE_REL_NS}}}Relationship" +_DEFAULT_TRANSITION_GAIN_DB = -9.1 +_DEFAULT_ANIMATION_GAIN_DB = -12.0 +_DEFAULT_LIMITER_DBFS = -1.0 +_OUTPUT_SAMPLE_RATE = 48_000 +_OUTPUT_AUDIO_BITRATE = "192k" +_START_TIME_TOLERANCE_MS = 1 +_VERIFICATION_SAMPLE_RATE = 8000 +_MIN_VERIFICATION_WINDOW_SAMPLES = 2 +_MIN_SOUND_MIX_CORRELATION = 0.20 + + +@dataclass(frozen=True) +class SoundCue: + """One resolved PPTX sound cue placed on the exported-video timeline.""" + + kind: str + slide_num: int + slide_name: str + row_index: int | None + shape_id: int | None + group_id: str | None + relationship_id: str + sound_name: str + package_part: str + media_sha256: str + media_path: Path + source_duration_ms: int + powerpoint_time_ms: int + video_time_ms: int + gain_db: float + + +@dataclass(frozen=True) +class SoundMixResult: + """Published files and cue count from one video sound mix.""" + + output_path: Path + stem_path: Path + report_path: Path + cue_count: int + verification_correlation: float + true_peak_dbfs: float + + +def _read_json_object(path: Path, label: str) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise ValueError(f"{label} is not valid JSON: {path}: {exc}") from exc + if not isinstance(value, dict): + raise ValueError(f"{label} must contain one JSON object: {path}") + return value + + +def _sha256_path(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _project_input_path(project_path: Path, value: str) -> Path: + path = Path(value).expanduser() + return (path if path.is_absolute() else project_path / path).resolve() + + +def _project_output_path( + project_path: Path, + value: str | None, + default: Path, +) -> Path: + path = Path(value).expanduser() if value else default + return (path if path.is_absolute() else project_path / path).resolve() + + +def _require_tool(name: str) -> str: + path = shutil.which(name) + if path is None: + raise RuntimeError( + f"Video sound mixing requires {name} on PATH. Install ffmpeg " + "and make both ffmpeg and ffprobe available." + ) + return path + + +def _run_command(command: list[str], label: str) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + capture_output=True, + check=False, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode != 0: + details = result.stderr.strip() or result.stdout.strip() + raise RuntimeError(f"{label} failed:\n{details}") + return result + + +def _probe_media(path: Path, ffprobe_path: str) -> dict[str, Any]: + result = _run_command( + [ + ffprobe_path, + "-v", + "error", + "-show_entries", + ( + "format=duration,start_time:" + "stream=index,codec_type,codec_name,time_base,start_pts," + "start_time,duration_ts,duration,sample_rate,channels," + "width,height,avg_frame_rate" + ), + "-of", + "json", + str(path), + ], + f"ffprobe inspection for {path}", + ) + try: + payload = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise RuntimeError(f"ffprobe returned invalid JSON for {path}") from exc + if not isinstance(payload, dict): + raise RuntimeError(f"ffprobe returned an invalid payload for {path}") + return payload + + +def _duration_ms(probe: dict[str, Any], label: str) -> int: + format_value = probe.get("format") + raw_duration = ( + format_value.get("duration") + if isinstance(format_value, dict) + else None + ) + try: + duration = float(raw_duration) + except (TypeError, ValueError) as exc: + raise ValueError(f"Unable to read {label} duration with ffprobe") from exc + if not math.isfinite(duration) or duration <= 0: + raise ValueError(f"{label} duration must be positive and finite") + return int(round(duration * 1000)) + + +def _stream_duration_ms( + stream: dict[str, Any], + *, + fallback_ms: int, +) -> int: + try: + duration = float(stream.get("duration")) + except (TypeError, ValueError): + return fallback_ms + if not math.isfinite(duration) or duration <= 0: + return fallback_ms + return int(round(duration * 1000)) + + +def _stream_start_ms(stream: dict[str, Any]) -> int: + try: + start = float(stream.get("start_time", 0)) + except (TypeError, ValueError): + return 0 + if not math.isfinite(start): + return 0 + return int(round(start * 1000)) + + +def _frame_duration_ms(stream: dict[str, Any]) -> float: + value = stream.get("avg_frame_rate") + if not isinstance(value, str) or "/" not in value: + return 1000 / 30 + numerator_text, denominator_text = value.split("/", 1) + try: + numerator = float(numerator_text) + denominator = float(denominator_text) + except ValueError: + return 1000 / 30 + if numerator <= 0 or denominator <= 0: + return 1000 / 30 + return 1000 * denominator / numerator + + +def _stream_timing_signature(stream: dict[str, Any]) -> dict[str, object]: + """Return the packet-clock fields that must survive video stream copy.""" + signature: dict[str, object] = {} + for key in ("time_base", "start_pts", "duration_ts"): + value = stream.get(key) + if value is None: + raise ValueError(f"Video stream is missing {key}") + signature[key] = value + return signature + + +def _audio_sample_count(stream: dict[str, Any]) -> int: + """Resolve an audio stream duration to exact samples from its time base.""" + try: + sample_rate = int(stream.get("sample_rate")) + duration_ts = int(stream.get("duration_ts")) + time_base = Fraction(str(stream.get("time_base"))) + except (TypeError, ValueError, ZeroDivisionError) as exc: + raise ValueError("Unable to read exact audio sample count") from exc + samples = duration_ts * time_base * sample_rate + if samples.denominator != 1: + raise ValueError( + "Audio duration does not resolve to a whole number of samples" + ) + return samples.numerator + + +def _target_sample_count(duration_ms: int) -> int: + return int(round(duration_ms * _OUTPUT_SAMPLE_RATE / 1000)) + + +def _streams(probe: dict[str, Any], kind: str) -> list[dict[str, Any]]: + raw_streams = probe.get("streams") + if not isinstance(raw_streams, list): + return [] + return [ + stream + for stream in raw_streams + if isinstance(stream, dict) and stream.get("codec_type") == kind + ] + + +def _stream_hash(path: Path, selector: str, ffmpeg_path: str) -> str: + result = _run_command( + [ + ffmpeg_path, + "-v", + "error", + "-i", + str(path), + "-map", + selector, + "-c", + "copy", + "-f", + "hash", + "-hash", + "sha256", + "-", + ], + f"stream hash for {path}", + ) + match = re.search(r"SHA256=([0-9a-fA-F]{64})", result.stdout) + if match is None: + raise RuntimeError(f"Unable to parse stream hash for {path}") + return match.group(1).lower() + + +def _maximum_volume_db(path: Path, ffmpeg_path: str) -> float | None: + result = subprocess.run( + [ + ffmpeg_path, + "-nostdin", + "-hide_banner", + "-i", + str(path), + "-map", + "0:a:0", + "-af", + "volumedetect", + "-f", + "null", + "-", + ], + capture_output=True, + check=False, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode != 0: + raise RuntimeError( + f"Volume inspection failed for {path}:\n{result.stderr.strip()}" + ) + match = re.search(r"max_volume:\s*(-?inf|[-+]?\d+(?:\.\d+)?)\s*dB", result.stderr) + if match is None: + raise RuntimeError(f"Unable to read maximum volume for {path}") + if match.group(1) == "-inf": + return None + return float(match.group(1)) + + +def _true_peak_dbfs(path: Path, ffmpeg_path: str) -> float | None: + result = subprocess.run( + [ + ffmpeg_path, + "-nostdin", + "-hide_banner", + "-nostats", + "-i", + str(path), + "-map", + "0:a:0", + "-af", + "ebur128=peak=true", + "-f", + "null", + "-", + ], + capture_output=True, + check=False, + text=True, + encoding="utf-8", + errors="replace", + ) + if result.returncode != 0: + raise RuntimeError( + f"True-peak inspection failed for {path}:\n{result.stderr.strip()}" + ) + matches = re.findall( + r"Peak:\s*(-?inf|[-+]?\d+(?:\.\d+)?)\s*dBFS", + result.stderr, + ) + if not matches: + raise RuntimeError(f"Unable to read true peak for {path}") + if matches[-1] == "-inf": + return None + return float(matches[-1]) + + +def _decode_audio_f32(path: Path, ffmpeg_path: str) -> Any: + try: + import numpy as np + except ImportError as exc: + raise RuntimeError( + "Video sound verification requires numpy. Install it with: " + "python3 -m pip install numpy" + ) from exc + result = subprocess.run( + [ + ffmpeg_path, + "-v", + "error", + "-i", + str(path), + "-map", + "0:a:0", + "-vn", + "-ac", + "1", + "-ar", + str(_VERIFICATION_SAMPLE_RATE), + "-f", + "f32le", + "-", + ], + capture_output=True, + check=False, + ) + if result.returncode != 0: + details = result.stderr.decode("utf-8", errors="replace").strip() + raise RuntimeError(f"Unable to decode audio for verification: {path}\n{details}") + return np.frombuffer(result.stdout, dtype=" float: + """Correlate the added final-audio component with the rendered SFX stem.""" + try: + import numpy as np + except ImportError as exc: + raise RuntimeError( + "Video sound verification requires numpy. Install it with: " + "python3 -m pip install numpy" + ) from exc + + target_samples = int(round(duration_ms * _VERIFICATION_SAMPLE_RATE / 1000)) + decoded = [ + _decode_audio_f32(path, ffmpeg_path) + for path in (raw_video_path, mixed_video_path, stem_path) + ] + arrays: list[Any] = [] + for samples in decoded: + if len(samples) < target_samples: + samples = np.pad(samples, (0, target_samples - len(samples))) + arrays.append(samples[:target_samples]) + raw_audio, mixed_audio, stem_audio = arrays + + cue_mask = np.zeros(target_samples, dtype=bool) + for cue in cues: + start = int(round(cue.video_time_ms * _VERIFICATION_SAMPLE_RATE / 1000)) + end = int( + round( + (cue.video_time_ms + cue.source_duration_ms) + * _VERIFICATION_SAMPLE_RATE + / 1000 + ) + ) + cue_mask[max(0, start):min(target_samples, end)] = True + cue_indices = np.flatnonzero(cue_mask) + if len(cue_indices) < _MIN_VERIFICATION_WINDOW_SAMPLES: + raise RuntimeError("SFX cue windows are too short for mix verification") + + background = ~cue_mask + raw_background = raw_audio[background] + mixed_background = mixed_audio[background] + denominator = float(np.dot(raw_background, raw_background)) + narration_scale = ( + float(np.dot(mixed_background, raw_background)) / denominator + if denominator > 1e-12 + else 1.0 + ) + added_audio = mixed_audio - narration_scale * raw_audio + + best_correlation = -1.0 + maximum_lag = max(1, _VERIFICATION_SAMPLE_RATE // 200) + for lag in range(-maximum_lag, maximum_lag + 1): + shifted_indices = cue_indices + lag + valid = (shifted_indices >= 0) & (shifted_indices < target_samples) + if not valid.any(): + continue + observed = added_audio[shifted_indices[valid]] + expected = stem_audio[cue_indices[valid]] + observed = observed - observed.mean() + expected = expected - expected.mean() + norm = float(np.linalg.norm(observed) * np.linalg.norm(expected)) + if norm <= 1e-12: + continue + correlation = float(np.dot(observed, expected) / norm) + best_correlation = max(best_correlation, correlation) + if best_correlation < _MIN_SOUND_MIX_CORRELATION: + raise RuntimeError( + "Mixed-video audio does not reliably contain the rendered SFX stem: " + f"correlation={best_correlation:.3f}, " + f"required>={_MIN_SOUND_MIX_CORRELATION:.2f}" + ) + return best_correlation + + +def _trace_slides(trace: dict[str, Any]) -> tuple[list[dict[str, Any]], list[str]]: + raw_slides = trace.get("slides") + if not isinstance(raw_slides, list) or not raw_slides: + raise ValueError("Conversion trace must contain a non-empty slides array") + slide_count = trace.get("slide_count") + if slide_count != len(raw_slides): + raise ValueError( + "Conversion trace slide_count does not match its slides array" + ) + + slides: list[dict[str, Any]] = [] + slide_names: list[str] = [] + for expected_number, raw_slide in enumerate(raw_slides, 1): + if not isinstance(raw_slide, dict): + raise ValueError( + f"Conversion trace slide {expected_number} must be an object" + ) + if raw_slide.get("slide_num") != expected_number: + raise ValueError( + "Conversion trace slides must be numbered consecutively from 1" + ) + raw_svg = raw_slide.get("svg") + if not isinstance(raw_svg, str) or not raw_svg.strip(): + raise ValueError( + f"Conversion trace slide {expected_number} has no SVG path" + ) + slide_name = Path(raw_svg).stem + if not slide_name: + raise ValueError( + f"Conversion trace slide {expected_number} has an invalid SVG path" + ) + slides.append(raw_slide) + slide_names.append(slide_name) + if len(set(slide_names)) != len(slide_names): + raise ValueError("Conversion trace slide names must be unique") + return slides, slide_names + + +def _shape_group_ids(trace_slide: dict[str, Any]) -> dict[int, str]: + events = trace_slide.get("events") + if not isinstance(events, list): + return {} + mapping: dict[int, str] = {} + for event in events: + if not isinstance(event, dict): + continue + shape_id = event.get("shape_id") + group_id = event.get("id") + if ( + isinstance(shape_id, int) + and not isinstance(shape_id, bool) + and isinstance(group_id, str) + and group_id + ): + mapping[shape_id] = group_id + return mapping + + +def _package_part_map(package: zipfile.ZipFile) -> dict[str, str]: + mapping: dict[str, str] = {} + for member in package.namelist(): + key = canonical_opc_part_path(member) + if key is None: + continue + prior = mapping.get(key) + if prior is not None and prior != member: + raise ValueError( + "PPTX contains OPC-equivalent duplicate package parts: " + f"{prior!r}, {member!r}" + ) + mapping[key] = member + return mapping + + +def _relationships_part(slide_part: str) -> str: + slide_path = Path(slide_part) + return ( + slide_path.parent / "_rels" / f"{slide_path.name}.rels" + ).as_posix() + + +def _slide_relationships( + package: zipfile.ZipFile, + rels_part: str, +) -> dict[str, ET.Element]: + try: + root = ET.fromstring(package.read(rels_part)) + except KeyError as exc: + raise ValueError(f"PPTX is missing slide relationships: {rels_part}") from exc + except ET.ParseError as exc: + raise ValueError(f"Invalid slide relationships XML: {rels_part}") from exc + relationships: dict[str, ET.Element] = {} + for relationship in root.findall(_RELATIONSHIP_TAG): + relationship_id = relationship.get("Id") + if not relationship_id: + continue + if relationship_id in relationships: + raise ValueError( + f"Duplicate relationship id {relationship_id!r} in {rels_part}" + ) + relationships[relationship_id] = relationship + return relationships + + +def _extract_sound( + *, + package: zipfile.ZipFile, + package_parts: dict[str, str], + relationships: dict[str, ET.Element], + rels_part: str, + relationship_id: str, + temp_dir: Path, + ffprobe_path: str, + extracted: dict[str, tuple[str, str, Path, int]], +) -> tuple[str, str, Path, int]: + relationship = relationships.get(relationship_id) + if relationship is None: + raise ValueError( + f"PPTX sound relationship is missing: {rels_part}#{relationship_id}" + ) + if relationship.get("Type") != AUDIO_REL_TYPE: + raise ValueError( + f"PPTX sound relationship is not audio: {rels_part}#{relationship_id}" + ) + if relationship.get("TargetMode", "Internal") == "External": + raise ValueError( + f"PPTX sound relationship must be internal: {rels_part}#{relationship_id}" + ) + target = relationship.get("Target") or "" + target_key = resolve_internal_opc_target(rels_part, target) + if target_key is None: + raise ValueError( + f"PPTX sound relationship target is invalid: {rels_part}#{relationship_id}" + ) + package_part = package_parts.get(target_key) + if package_part is None: + raise ValueError( + f"PPTX sound relationship target is missing: {target!r}" + ) + cached = extracted.get(package_part) + if cached is not None: + return cached + + payload = package.read(package_part) + media_sha256 = hashlib.sha256(payload).hexdigest() + extension = Path(package_part).suffix.lower() + if extension not in {".m4a", ".mp3", ".wav"}: + raise ValueError( + f"PPTX animation sound uses an unsupported media type: {package_part}" + ) + media_path = temp_dir / f"{media_sha256}{extension}" + media_path.write_bytes(payload) + source_duration_ms = _duration_ms( + _probe_media(media_path, ffprobe_path), + f"PPTX sound {package_part}", + ) + resolved = (package_part, media_sha256, media_path, source_duration_ms) + extracted[package_part] = resolved + return resolved + + +def _require_equal(actual: object, expected: object, field: str) -> None: + if actual != expected: + raise ValueError( + f"Final narrated trace does not match the PPTX at {field}: " + f"trace={actual!r}, pptx={expected!r}" + ) + + +def _resolve_sound_cues( + *, + pptx_path: Path, + trace_slides: list[dict[str, Any]], + calibration: VideoTimelineCalibration, + transition_gain_db: float, + animation_gain_db: float, + video_duration_ms: int, + temp_dir: Path, + ffprobe_path: str, +) -> list[SoundCue]: + cues: list[SoundCue] = [] + extracted: dict[str, tuple[str, str, Path, int]] = {} + with zipfile.ZipFile(pptx_path) as package: + slide_members = presentation_slide_members(package) + if len(slide_members) != len(trace_slides): + raise ValueError( + f"Final narrated PPTX has {len(slide_members)} slides, but " + f"the trace has {len(trace_slides)}" + ) + package_parts = _package_part_map(package) + + for slide_num, (slide_part, trace_slide, calibrated) in enumerate( + zip(slide_members, trace_slides, calibration.slides), + 1, + ): + slide_xml = package.read(slide_part) + transition = read_slide_transition_xml(slide_xml) + animation = read_slide_animation_sequence( + slide_xml, + require_supported_effects=True, + ) + trace_motion = trace_slide.get("motion") + trace_animation = trace_slide.get("animation") + if not isinstance(trace_motion, dict) or not isinstance( + trace_animation, dict + ): + raise ValueError( + f"Conversion trace slide {slide_num} lacks resolved motion" + ) + + _require_equal( + trace_motion.get("duration_ms"), + transition.duration_ms, + f"slide {slide_num} transition duration", + ) + _require_equal( + trace_motion.get("advance_after_ms"), + transition.advance_after_ms, + f"slide {slide_num} recorded advance", + ) + _require_equal( + trace_motion.get("sound_relationship_id"), + transition.sound_relationship_id, + f"slide {slide_num} transition sound relationship", + ) + _require_equal( + trace_motion.get("sound_name"), + transition.sound_name, + f"slide {slide_num} transition sound name", + ) + _require_equal( + trace_motion.get("fallback_sound_relationship_id"), + transition.fallback_sound_relationship_id, + f"slide {slide_num} fallback transition sound relationship", + ) + _require_equal( + trace_motion.get("fallback_sound_name"), + transition.fallback_sound_name, + f"slide {slide_num} fallback transition sound name", + ) + if transition.fallback_sound_relationship_id not in { + None, + transition.sound_relationship_id, + }: + raise ValueError( + f"Slide {slide_num} transition primary and fallback " + "branches reference different sounds" + ) + + raw_rows = trace_animation.get("rows") + if not isinstance(raw_rows, list): + raise ValueError( + f"Conversion trace slide {slide_num} animation rows are invalid" + ) + if len(raw_rows) != len(animation.rows): + raise ValueError( + f"Final narrated trace has {len(raw_rows)} animation rows " + f"on slide {slide_num}; PPTX has {len(animation.rows)}" + ) + for row_index, (raw_row, actual_row) in enumerate( + zip(raw_rows, animation.rows), + 1, + ): + if not isinstance(raw_row, dict): + raise ValueError( + f"Conversion trace slide {slide_num} row {row_index} " + "must be an object" + ) + for key, expected in ( + ("shape_id", actual_row.shape_id), + ("offset_ms", actual_row.offset_ms), + ("trigger", actual_row.trigger), + ("sound_relationship_id", actual_row.sound_relationship_id), + ("sound_name", actual_row.sound_name), + ): + _require_equal( + raw_row.get(key), + expected, + f"slide {slide_num} animation row {row_index} {key}", + ) + + sound_relationship_ids = { + relationship_id + for relationship_id in ( + transition.sound_relationship_id, + *( + row.sound_relationship_id + for row in animation.rows + ), + ) + if relationship_id is not None + } + relationships: dict[str, ET.Element] = {} + rels_part = _relationships_part(slide_part) + if sound_relationship_ids: + relationships = _slide_relationships(package, rels_part) + + group_ids = _shape_group_ids(trace_slide) + if transition.sound_relationship_id is not None: + package_part, media_sha256, media_path, source_duration_ms = ( + _extract_sound( + package=package, + package_parts=package_parts, + relationships=relationships, + rels_part=rels_part, + relationship_id=transition.sound_relationship_id, + temp_dir=temp_dir, + ffprobe_path=ffprobe_path, + extracted=extracted, + ) + ) + cues.append( + SoundCue( + kind="transition", + slide_num=slide_num, + slide_name=calibrated.slide_name, + row_index=None, + shape_id=None, + group_id=None, + relationship_id=transition.sound_relationship_id, + sound_name=transition.sound_name or Path(package_part).name, + package_part=package_part, + media_sha256=media_sha256, + media_path=media_path, + source_duration_ms=source_duration_ms, + powerpoint_time_ms=calibrated.powerpoint_slide_start_ms, + video_time_ms=calibrated.video_slide_start_ms, + gain_db=transition_gain_db, + ) + ) + + for row_index, row in enumerate(animation.rows, 1): + if row.sound_relationship_id is None: + continue + if row.trigger not in {"after-previous", "with-previous"}: + raise ValueError( + f"Slide {slide_num} animation row {row_index} uses " + f"{row.trigger!r}; video sound timing requires a " + "click-free row" + ) + package_part, media_sha256, media_path, source_duration_ms = ( + _extract_sound( + package=package, + package_parts=package_parts, + relationships=relationships, + rels_part=rels_part, + relationship_id=row.sound_relationship_id, + temp_dir=temp_dir, + ffprobe_path=ffprobe_path, + extracted=extracted, + ) + ) + powerpoint_time_ms = ( + calibrated.powerpoint_slide_start_ms + + calibrated.transition_ms + + row.offset_ms + ) + video_time_ms = ( + calibrated.video_slide_start_ms + + calibrated.transition_ms + + row.offset_ms + ) + cues.append( + SoundCue( + kind="animation", + slide_num=slide_num, + slide_name=calibrated.slide_name, + row_index=row_index, + shape_id=row.shape_id, + group_id=group_ids.get(row.shape_id), + relationship_id=row.sound_relationship_id, + sound_name=row.sound_name or Path(package_part).name, + package_part=package_part, + media_sha256=media_sha256, + media_path=media_path, + source_duration_ms=source_duration_ms, + powerpoint_time_ms=powerpoint_time_ms, + video_time_ms=video_time_ms, + gain_db=animation_gain_db, + ) + ) + + cues.sort( + key=lambda cue: ( + cue.video_time_ms, + cue.slide_num, + 0 if cue.kind == "transition" else 1, + cue.row_index or 0, + ) + ) + if not cues: + raise ValueError( + "Final narrated PPTX and trace contain no transition or " + "object-animation sound cues; use the PowerPoint video directly" + ) + for cue in cues: + if cue.video_time_ms < 0 or cue.video_time_ms >= video_duration_ms: + raise ValueError( + f"Sound cue falls outside the video: slide {cue.slide_num}, " + f"time={cue.video_time_ms}ms, video={video_duration_ms}ms" + ) + cue_end_ms = cue.video_time_ms + cue.source_duration_ms + if cue_end_ms > video_duration_ms: + raise ValueError( + f"Sound cue would be truncated by the video end: slide " + f"{cue.slide_num}, cue end={cue_end_ms}ms, " + f"video={video_duration_ms}ms" + ) + return cues + + +def _render_stem( + *, + cues: list[SoundCue], + duration_ms: int, + output_path: Path, + ffmpeg_path: str, +) -> None: + target_samples = _target_sample_count(duration_ms) + command = [ + ffmpeg_path, + "-nostdin", + "-hide_banner", + "-v", + "error", + "-f", + "lavfi", + "-i", + f"anullsrc=r={_OUTPUT_SAMPLE_RATE}:cl=stereo", + ] + for cue in cues: + command.extend(["-i", str(cue.media_path)]) + + filters = [ + ( + f"[0:a:0]aformat=sample_fmts=fltp:sample_rates={_OUTPUT_SAMPLE_RATE}:" + "channel_layouts=stereo," + f"atrim=end_sample={target_samples},asetpts=N/SR/TB[base]" + ) + ] + labels = ["[base]"] + for input_index, cue in enumerate(cues, 1): + label = f"cue{input_index}" + delay_samples = int( + round(cue.video_time_ms * _OUTPUT_SAMPLE_RATE / 1000) + ) + filters.append( + f"[{input_index}:a:0]aresample={_OUTPUT_SAMPLE_RATE}," + "aformat=sample_fmts=fltp:" + f"sample_rates={_OUTPUT_SAMPLE_RATE}:channel_layouts=stereo," + f"asetpts=N/SR/TB,volume={cue.gain_db:.3f}dB," + f"adelay={delay_samples}S:all=1," + f"apad=whole_len={target_samples}," + f"atrim=end_sample={target_samples}[{label}]" + ) + labels.append(f"[{label}]") + filters.append( + "".join(labels) + + f"amix=inputs={len(labels)}:duration=first:" + "dropout_transition=0:normalize=0," + f"atrim=end_sample={target_samples},asetpts=N/SR/TB[stem]" + ) + command.extend( + [ + "-filter_complex", + ";".join(filters), + "-map", + "[stem]", + "-c:a", + "pcm_f32le", + "-ar", + str(_OUTPUT_SAMPLE_RATE), + "-ac", + "2", + "-y", + str(output_path), + ] + ) + _run_command(command, "SFX stem render") + + +def _mix_video( + *, + video_path: Path, + stem_path: Path, + duration_ms: int, + limiter_dbfs: float, + output_path: Path, + ffmpeg_path: str, +) -> None: + target_samples = _target_sample_count(duration_ms) + limiter_ratio = 10 ** (limiter_dbfs / 20) + filter_graph = ( + f"[0:a:0]aresample={_OUTPUT_SAMPLE_RATE}:first_pts=0," + "aformat=sample_fmts=fltp:channel_layouts=stereo," + f"asetpts=N/SR/TB,apad=whole_len={target_samples}," + f"atrim=end_sample={target_samples}[narration];" + f"[1:a:0]aresample={_OUTPUT_SAMPLE_RATE}:first_pts=0," + "aformat=sample_fmts=fltp:channel_layouts=stereo," + f"asetpts=N/SR/TB,apad=whole_len={target_samples}," + f"atrim=end_sample={target_samples}[sfx];" + "[narration][sfx]amix=inputs=2:duration=first:" + "dropout_transition=0:normalize=0," + f"alimiter=limit={limiter_ratio:.9f}:attack=5:release=50:" + "level=false:latency=true," + f"apad=whole_len={target_samples}," + f"atrim=end_sample={target_samples},asetpts=N/SR/TB[mixed]" + ) + _run_command( + [ + ffmpeg_path, + "-nostdin", + "-hide_banner", + "-v", + "error", + "-i", + str(video_path), + "-i", + str(stem_path), + "-filter_complex", + filter_graph, + "-map", + "0:v:0", + "-map", + "[mixed]", + "-map_metadata", + "0", + "-c:v", + "copy", + "-c:a", + "aac", + "-b:a", + _OUTPUT_AUDIO_BITRATE, + "-ar", + str(_OUTPUT_SAMPLE_RATE), + "-ac", + "2", + "-movflags", + "+faststart", + "-y", + str(output_path), + ], + "Narration and SFX mix", + ) + + +def _temporary_output(path: Path, suffix: str) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, raw_path = tempfile.mkstemp( + prefix=f".{path.stem}.", + suffix=suffix, + dir=path.parent, + ) + os.close(descriptor) + temporary = Path(raw_path) + temporary.unlink() + return temporary + + +def _publish_outputs(replacements: list[tuple[Path, Path]]) -> None: + """Publish verified outputs together, restoring prior files on failure.""" + backups: dict[Path, Path] = {} + published: list[Path] = [] + try: + for _temporary, destination in replacements: + if not destination.exists(): + continue + backup = _temporary_output(destination, f".backup{destination.suffix}") + os.replace(destination, backup) + backups[destination] = backup + for temporary, destination in replacements: + os.replace(temporary, destination) + published.append(destination) + except OSError as exc: + rollback_errors: list[str] = [] + for destination in reversed(published): + backup = backups.pop(destination, None) + try: + if backup is None: + destination.unlink(missing_ok=True) + else: + os.replace(backup, destination) + except OSError as rollback_exc: + rollback_errors.append(f"{destination}: {rollback_exc}") + for destination, backup in backups.items(): + try: + os.replace(backup, destination) + except OSError as rollback_exc: + rollback_errors.append(f"{destination}: {rollback_exc}") + if rollback_errors: + raise RuntimeError( + "Output publication failed and rollback was incomplete: " + + "; ".join(rollback_errors) + ) from exc + raise + else: + for backup in backups.values(): + backup.unlink(missing_ok=True) + + +def _cue_report(cue: SoundCue) -> dict[str, Any]: + return { + "kind": cue.kind, + "slide_num": cue.slide_num, + "slide_name": cue.slide_name, + "row_index": cue.row_index, + "shape_id": cue.shape_id, + "group_id": cue.group_id, + "relationship_id": cue.relationship_id, + "sound_name": cue.sound_name, + "package_part": cue.package_part, + "media_sha256": cue.media_sha256, + "source_duration_ms": cue.source_duration_ms, + "powerpoint_time_ms": cue.powerpoint_time_ms, + "video_time_ms": cue.video_time_ms, + "gain_db": cue.gain_db, + } + + +def mix_video_sounds( + project_path: Path, + *, + pptx_path: Path, + trace_path: Path, + video_path: Path, + audio_dir: Path, + subtitle_dir: Path | None, + output_path: Path, + stem_path: Path, + report_path: Path, + transition_gain_db: float = _DEFAULT_TRANSITION_GAIN_DB, + animation_gain_db: float = _DEFAULT_ANIMATION_GAIN_DB, + limiter_dbfs: float = _DEFAULT_LIMITER_DBFS, + force: bool = False, +) -> SoundMixResult: + """Render and publish one calibrated animation-sound video mix.""" + project_path = project_path.resolve() + pptx_path = pptx_path.resolve() + trace_path = trace_path.resolve() + video_path = video_path.resolve() + audio_dir = audio_dir.resolve() + subtitle_dir = subtitle_dir.resolve() if subtitle_dir is not None else None + output_path = output_path.resolve() + stem_path = stem_path.resolve() + report_path = report_path.resolve() + if not project_path.is_dir(): + raise FileNotFoundError(f"Project path does not exist: {project_path}") + for label, path in ( + ("Final narrated PPTX", pptx_path), + ("Final narrated conversion trace", trace_path), + ("Raw PowerPoint video", video_path), + ): + if not path.is_file(): + raise FileNotFoundError(f"{label} does not exist: {path}") + if pptx_path.suffix.lower() != ".pptx": + raise ValueError(f"Final narrated PPTX must use .pptx: {pptx_path}") + if video_path.suffix.lower() != ".mp4": + raise ValueError(f"Raw PowerPoint video must use .mp4: {video_path}") + if output_path.suffix.lower() != ".mp4": + raise ValueError(f"Mixed video output must use .mp4: {output_path}") + if stem_path.suffix.lower() != ".wav": + raise ValueError(f"SFX stem output must use .wav: {stem_path}") + if report_path.suffix.lower() != ".json": + raise ValueError(f"Sound mix report must use .json: {report_path}") + + outputs = [output_path, stem_path, report_path] + if len(set(outputs)) != len(outputs): + raise ValueError("Mixed video, SFX stem, and report paths must differ") + invalid_outputs = [ + path for path in outputs if path.exists() and not path.is_file() + ] + if invalid_outputs: + raise ValueError( + "Sound mix outputs must be regular files or unused paths: " + + ", ".join(str(path) for path in invalid_outputs) + ) + existing = [path for path in outputs if path.exists()] + if existing and not force: + raise FileExistsError( + "Sound mix output already exists; use --force to replace it: " + + ", ".join(str(path) for path in existing) + ) + for label, value in ( + ("transition gain", transition_gain_db), + ("animation gain", animation_gain_db), + ): + if not math.isfinite(value) or value < -60 or value > 0: + raise ValueError(f"{label} must be finite and between -60 and 0 dB") + if not math.isfinite(limiter_dbfs) or not -12 <= limiter_dbfs < 0: + raise ValueError("limiter ceiling must be finite, at least -12, and below 0 dBFS") + + ffmpeg_path = _require_tool("ffmpeg") + ffprobe_path = _require_tool("ffprobe") + trace = _read_json_object(trace_path, "Conversion trace") + trace_slides, slide_names = _trace_slides(trace) + raw_probe = _probe_media(video_path, ffprobe_path) + raw_format_duration_ms = _duration_ms(raw_probe, "raw PowerPoint video") + raw_video_streams = _streams(raw_probe, "video") + raw_audio_streams = _streams(raw_probe, "audio") + if len(raw_video_streams) != 1 or len(raw_audio_streams) != 1: + raise ValueError( + "Raw PowerPoint video must contain one video stream and one " + "narration audio stream" + ) + raw_duration_ms = _stream_duration_ms( + raw_video_streams[0], + fallback_ms=raw_format_duration_ms, + ) + raw_video_start_ms = _stream_start_ms(raw_video_streams[0]) + raw_audio_start_ms = _stream_start_ms(raw_audio_streams[0]) + start_delta_ms = abs(raw_video_start_ms - raw_audio_start_ms) + frame_duration_ms = _frame_duration_ms(raw_video_streams[0]) + if ( + abs(raw_video_start_ms) > _START_TIME_TOLERANCE_MS + or abs(raw_audio_start_ms) > _START_TIME_TOLERANCE_MS + or start_delta_ms > _START_TIME_TOLERANCE_MS + ): + raise ValueError( + "Raw PowerPoint video audio and video streams must both start at " + f"zero: video={raw_video_start_ms}ms, audio={raw_audio_start_ms}ms" + ) + raw_video_timing = _stream_timing_signature(raw_video_streams[0]) + + calibration = calibrate_video_timeline( + slide_names=slide_names, + pptx_path=pptx_path, + audio_dir=audio_dir, + video_path=video_path, + subtitle_dir=subtitle_dir, + ) + protected_inputs = { + pptx_path, + trace_path, + video_path, + *(slide.audio_path.resolve() for slide in calibration.slides), + } + aliases = protected_inputs.intersection(outputs) + if aliases: + raise ValueError( + "Sound mix outputs must not overwrite inputs: " + + ", ".join(str(path) for path in sorted(aliases)) + ) + temporary_stem = _temporary_output(stem_path, ".wav") + temporary_video = _temporary_output(output_path, ".mp4") + temporary_report = _temporary_output(report_path, ".json") + try: + with tempfile.TemporaryDirectory(prefix="ppt-master-video-sounds-") as raw_dir: + cues = _resolve_sound_cues( + pptx_path=pptx_path, + trace_slides=trace_slides, + calibration=calibration, + transition_gain_db=transition_gain_db, + animation_gain_db=animation_gain_db, + video_duration_ms=raw_duration_ms, + temp_dir=Path(raw_dir), + ffprobe_path=ffprobe_path, + ) + _render_stem( + cues=cues, + duration_ms=raw_duration_ms, + output_path=temporary_stem, + ffmpeg_path=ffmpeg_path, + ) + stem_peak_db = _maximum_volume_db(temporary_stem, ffmpeg_path) + if stem_peak_db is None or stem_peak_db < -90: + raise RuntimeError("Rendered SFX stem is silent") + _mix_video( + video_path=video_path, + stem_path=temporary_stem, + duration_ms=raw_duration_ms, + limiter_dbfs=limiter_dbfs, + output_path=temporary_video, + ffmpeg_path=ffmpeg_path, + ) + + stem_probe = _probe_media(temporary_stem, ffprobe_path) + mixed_probe = _probe_media(temporary_video, ffprobe_path) + stem_duration_ms = _duration_ms(stem_probe, "SFX stem") + mixed_duration_ms = _duration_ms(mixed_probe, "mixed video") + mixed_video_streams = _streams(mixed_probe, "video") + mixed_audio_streams = _streams(mixed_probe, "audio") + if len(mixed_video_streams) != 1 or len(mixed_audio_streams) != 1: + raise RuntimeError( + "Mixed video must contain one copied video stream and one audio stream" + ) + mixed_video_duration_ms = _stream_duration_ms( + mixed_video_streams[0], + fallback_ms=mixed_duration_ms, + ) + mixed_audio_duration_ms = _stream_duration_ms( + mixed_audio_streams[0], + fallback_ms=mixed_duration_ms, + ) + target_samples = _target_sample_count(raw_duration_ms) + stem_samples = _audio_sample_count(_streams(stem_probe, "audio")[0]) + if stem_samples != target_samples: + raise RuntimeError( + "SFX stem sample count does not match the raw video target: " + f"stem={stem_samples}, target={target_samples}" + ) + mixed_video_timing = _stream_timing_signature(mixed_video_streams[0]) + if mixed_video_timing != raw_video_timing: + raise RuntimeError( + "Mixed output changed the PowerPoint video packet timeline: " + f"raw={raw_video_timing!r}, mixed={mixed_video_timing!r}" + ) + duration_tolerance_ms = max( + frame_duration_ms, + 1024 * 1000 / _OUTPUT_SAMPLE_RATE, + ) + 1 + if abs(mixed_audio_duration_ms - raw_duration_ms) > duration_tolerance_ms: + raise RuntimeError( + "Mixed audio duration does not match the raw video: " + f"audio={mixed_audio_duration_ms}ms, raw={raw_duration_ms}ms" + ) + + raw_video_hash = _stream_hash(video_path, "0:v:0", ffmpeg_path) + mixed_video_hash = _stream_hash(temporary_video, "0:v:0", ffmpeg_path) + if raw_video_hash != mixed_video_hash: + raise RuntimeError("Mixed output changed the PowerPoint video stream") + raw_audio_hash = _stream_hash(video_path, "0:a:0", ffmpeg_path) + mixed_audio_hash = _stream_hash(temporary_video, "0:a:0", ffmpeg_path) + if raw_audio_hash == mixed_audio_hash: + raise RuntimeError("Mixed output audio is unchanged from the raw video") + mixed_peak_db = _maximum_volume_db(temporary_video, ffmpeg_path) + true_peak_dbfs = _true_peak_dbfs(temporary_video, ffmpeg_path) + if true_peak_dbfs is None or true_peak_dbfs >= 0: + raise RuntimeError( + "Mixed-video audio is silent or clips after encoding: " + f"true_peak={true_peak_dbfs!r} dBFS" + ) + verification_correlation = _sound_mix_correlation( + raw_video_path=video_path, + mixed_video_path=temporary_video, + stem_path=temporary_stem, + cues=cues, + duration_ms=raw_duration_ms, + ffmpeg_path=ffmpeg_path, + ) + + trace_output = trace.get("output") + trace_output_matches_pptx = False + if isinstance(trace_output, str) and trace_output.strip(): + trace_output_matches_pptx = Path(trace_output).expanduser().resolve() == pptx_path + + report = { + "schema": "ppt-master.video-sound-mix-report.v1", + "version": 1, + "status": "passed", + "inputs": { + "pptx": { + "path": str(pptx_path), + "sha256": _sha256_path(pptx_path), + }, + "conversion_trace": { + "path": str(trace_path), + "sha256": _sha256_path(trace_path), + "declared_output_matches_pptx": trace_output_matches_pptx, + }, + "raw_video": { + "path": str(video_path), + "sha256": _sha256_path(video_path), + "duration_ms": raw_duration_ms, + "video_start_ms": _stream_start_ms(raw_video_streams[0]), + "audio_start_ms": _stream_start_ms(raw_audio_streams[0]), + "video_stream_sha256": raw_video_hash, + "video_timing": raw_video_timing, + "audio_stream_sha256": raw_audio_hash, + "video_codec": raw_video_streams[0].get("codec_name"), + "audio_codec": raw_audio_streams[0].get("codec_name"), + }, + }, + "settings": { + "transition_gain_db": transition_gain_db, + "animation_gain_db": animation_gain_db, + "limiter_dbfs": limiter_dbfs, + "mix_normalize": False, + "ducking": False, + "background_music": False, + "sample_rate": _OUTPUT_SAMPLE_RATE, + "audio_bitrate": _OUTPUT_AUDIO_BITRATE, + }, + "timeline": { + "method": "page narration correlation against raw video audio", + "powerpoint_timeline_ms": calibration.powerpoint_timeline_ms, + "minimum_correlation": min( + slide.correlation for slide in calibration.slides + ), + "slides": [ + { + "slide_num": index, + "slide_name": slide.slide_name, + "powerpoint_slide_start_ms": slide.powerpoint_slide_start_ms, + "powerpoint_narration_start_ms": ( + slide.powerpoint_narration_start_ms + ), + "video_slide_start_ms": slide.video_slide_start_ms, + "video_narration_start_ms": slide.video_narration_start_ms, + "adjustment_ms": slide.adjustment_ms, + "correlation": slide.correlation, + } + for index, slide in enumerate(calibration.slides, 1) + ], + }, + "cues": [_cue_report(cue) for cue in cues], + "outputs": { + "sfx_stem": { + "path": str(stem_path), + "sha256": _sha256_path(temporary_stem), + "duration_ms": stem_duration_ms, + "sample_count": stem_samples, + "maximum_volume_db": stem_peak_db, + }, + "mixed_video": { + "path": str(output_path), + "sha256": _sha256_path(temporary_video), + "duration_ms": mixed_duration_ms, + "video_duration_ms": mixed_video_duration_ms, + "audio_duration_ms": mixed_audio_duration_ms, + "video_stream_sha256": mixed_video_hash, + "video_timing": mixed_video_timing, + "audio_stream_sha256": mixed_audio_hash, + "video_codec": mixed_video_streams[0].get("codec_name"), + "audio_codec": mixed_audio_streams[0].get("codec_name"), + "maximum_volume_db": mixed_peak_db, + "true_peak_dbfs": true_peak_dbfs, + }, + }, + "validation": { + "trace_matches_pptx_motion": True, + "cue_count": len(cues), + "stem_non_silent": True, + "mixed_audio_present": True, + "video_stream_preserved": True, + "duration_delta_ms": mixed_video_duration_ms - raw_duration_ms, + "video_timeline_preserved": True, + "limiter_applied": True, + "sound_mix_correlation": verification_correlation, + "minimum_sound_mix_correlation": _MIN_SOUND_MIX_CORRELATION, + }, + } + temporary_report.write_text( + json.dumps(report, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + _publish_outputs( + [ + (temporary_stem, stem_path), + (temporary_video, output_path), + (temporary_report, report_path), + ] + ) + finally: + for temporary in (temporary_stem, temporary_video, temporary_report): + temporary.unlink(missing_ok=True) + + return SoundMixResult( + output_path=output_path, + stem_path=stem_path, + report_path=report_path, + cue_count=len(cues), + verification_correlation=verification_correlation, + true_peak_dbfs=true_peak_dbfs, + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description=( + "Mix PPTX transition/object-animation sounds into a " + "PowerPoint-exported narrated video." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("project_path", help="PPT Master project directory") + parser.add_argument( + "--pptx", + required=True, + help="Final narrated PPTX; relative paths resolve under the project", + ) + parser.add_argument( + "--trace", + required=True, + help="Final narrated conversion trace; relative paths resolve under the project", + ) + parser.add_argument( + "--video", + required=True, + help="Raw PowerPoint-exported video; relative paths resolve under the project", + ) + parser.add_argument( + "--audio-dir", + default=None, + help="Page narration audio directory; default: /audio", + ) + parser.add_argument( + "--subtitle-dir", + default=None, + help=( + "Optional page-local SRT directory used to refine calibration; " + "default: /audio" + ), + ) + parser.add_argument( + "-o", + "--output", + default=None, + help="Final mixed MP4; default: beside raw video as *_mixed.mp4", + ) + parser.add_argument( + "--stem-output", + default=None, + help="Independent SFX WAV; default: beside raw video as *_sfx.wav", + ) + parser.add_argument( + "--report-output", + default=None, + help="Sound mix JSON receipt; default: beside raw video as *_sound_mix.json", + ) + parser.add_argument( + "--transition-gain-db", + type=float, + default=_DEFAULT_TRANSITION_GAIN_DB, + help="Transition cue gain in dB (default: -9.1, about 35%%)", + ) + parser.add_argument( + "--animation-gain-db", + type=float, + default=_DEFAULT_ANIMATION_GAIN_DB, + help="Object-animation cue gain in dB (default: -12, about 25%%)", + ) + parser.add_argument( + "--limiter-dbfs", + type=float, + default=_DEFAULT_LIMITER_DBFS, + help="Final peak limiter ceiling in dBFS (default: -1)", + ) + parser.add_argument( + "--force", + action="store_true", + help="Replace existing mixed video, stem, and report outputs", + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + project_path = Path(args.project_path).expanduser().resolve() + if not project_path.is_dir(): + parser.error(f"Project path does not exist: {project_path}") + + pptx_path = _project_input_path(project_path, args.pptx) + trace_path = _project_input_path(project_path, args.trace) + video_path = _project_input_path(project_path, args.video) + audio_dir = _project_output_path( + project_path, + args.audio_dir, + Path("audio"), + ) + subtitle_dir = _project_output_path( + project_path, + args.subtitle_dir, + Path("audio"), + ) + default_output = video_path.with_name(f"{video_path.stem}_mixed.mp4") + default_stem = video_path.with_name(f"{video_path.stem}_sfx.wav") + default_report = video_path.with_name(f"{video_path.stem}_sound_mix.json") + output_path = _project_output_path(project_path, args.output, default_output) + stem_path = _project_output_path( + project_path, + args.stem_output, + default_stem, + ) + report_path = _project_output_path( + project_path, + args.report_output, + default_report, + ) + + try: + result = mix_video_sounds( + project_path, + pptx_path=pptx_path, + trace_path=trace_path, + video_path=video_path, + audio_dir=audio_dir, + subtitle_dir=subtitle_dir, + output_path=output_path, + stem_path=stem_path, + report_path=report_path, + transition_gain_db=args.transition_gain_db, + animation_gain_db=args.animation_gain_db, + limiter_dbfs=args.limiter_dbfs, + force=args.force, + ) + except (FileNotFoundError, FileExistsError, OSError, RuntimeError, ValueError) as exc: + print(f"Video sound mix failed: {exc}", file=sys.stderr) + return 1 + + print( + f"[POSTFLIGHT] status=passed cues={result.cue_count} " + "video_stream=preserved audio_mix=verified " + f"correlation={result.verification_correlation:.3f} " + f"true_peak={result.true_peak_dbfs:.1f}dBFS" + ) + print(f"[Done] Mixed video: {result.output_path}") + print(f"[Done] SFX stem: {result.stem_path}") + print(f"[REPORT] Video sound mix: {result.report_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_subtitles.py b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_subtitles.py index 9fe6f05c..62e6b02d 100644 --- a/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_subtitles.py +++ b/plugins/codex/plugins/ppt-master/skills/ppt-master/scripts/video_subtitles.py @@ -2,10 +2,10 @@ """ PPT Master - Final Video Subtitles -Align the exact narration text frozen in page-local narration SRT files against the -audio track of a finished PowerPoint-exported video. This produces a delivery -SRT from the actual video timeline without rewriting speaker notes or relying -on theoretical slide offsets. +Align the exact narration text frozen in page-local narration SRT files against +the audio track of a finished PowerPoint-exported or slideshow-captured video. +This produces a delivery SRT from the actual video timeline without rewriting +speaker notes or relying on theoretical slide offsets. Usage: python3 scripts/video_subtitles.py --video

YB^uzfF(SBgd=J(R<2lfz7P!>J>^gDnxtGqx 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 DuX1|C literal 0 HcmV?d00001 diff --git a/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/0573.wav b/plugins/codex/plugins/ppt-master/skills/ppt-master/templates/sounds/bigsoundbank/0573.wav new file mode 100644 index 0000000000000000000000000000000000000000..b409ff958a9c89335a77fa84ffe2e93ac625ba3c GIT binary patch literal 1084718 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�k-skn<%>Y= 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