| 1 | --- |
| 2 | description: Shared post-processing stage for narration audio, PPTX embedding, PowerPoint video delivery, and triggered sound mixing. |
| 3 | --- |
| 4 | |
| 5 | # Generate Audio Stage |
| 6 | |
| 7 | > Shared narration stage. Run after the owning route's notes step. Edge, ElevenLabs, MiniMax, and timestamp-capable CosyVoice produce per-slide audio/SRT from synthesis timing. Qwen is audio-only because its TTS API exposes no timing. The caller owns final PPTX integration. |
| 8 | |
| 9 | This stage is **context-independent**: it reads `notes/*.md` and queries the selected TTS voice catalog, so either owning route may invoke it in a fresh session. It does not choose the top-level route and does not patch slide design. |
| 10 | |
| 11 | **Trigger**: In Generate PPTX, run when the effective `Narration Audio` outcome |
| 12 | in `design_spec.md` §I is `enabled`; a later explicit request first updates that |
| 13 | outcome and its provenance. Quick Generate instead runs when the request or |
| 14 | current agent's active-context decision selects narration. In Enhance Native |
| 15 | PPTX, run when its confirmed enhancement plan has `audio.enabled: true`. |
| 16 | |
| 17 | **Hard dependency — speaker notes**: Audio requires complete per-slide speaker |
| 18 | notes. Generate PPTX additionally requires its effective `Speaker Notes` |
| 19 | outcome to be enabled; Enhance Native PPTX follows its confirmed notes/audio |
| 20 | plan, where enabling audio also enables notes. Quick records the same dependency |
| 21 | in active context. Do not enter audio generation while the owning route's notes |
| 22 | are missing or incomplete; generate and validate those notes first, then resume |
| 23 | this stage. |
| 24 | |
| 25 | ## When to Run |
| 26 | |
| 27 | - Per-page narration files exist at `notes/*.md`. In Generate PPTX, split `notes/total.md` during Step 7.1. In Enhance Native PPTX, the notes module writes numeric files such as `001.md`. |
| 28 | - Default mode: `edge-tts` is installed (`python3 -m pip install edge-tts`). |
| 29 | - The stage is page-level only: one note becomes `audio/<stem>.<audio-ext>` plus `audio/<stem>.srt` on provider-timed paths, or one audio file with Qwen / explicit CosyVoice audio-only mode. Never substitute one long track or automatic splitting. |
| 30 | - Final/literal script notes are synthesized verbatim. Source SRT timecodes are pacing evidence only; new provider timing owns the generated audio/SRT set. |
| 31 | - SRT bound to an authoritative existing recording does not enter TTS. Recorded narration requires page-level audio or an explicit page/time map; automatic long-track splitting is unsupported. |
| 32 | - A fully successful run writes a compact `audio/manifest.json` with only provider/model, audio/subtitle format, relevant voice settings, and a SHA-256 fingerprint instead of the raw cloud voice ID. It has no per-slide inventory, artifact hashes, or API keys and is not a normal generation input. The flat `audio/` directory is the single active narration set; do not create provider subdirectories unless the user explicitly asks to preserve multiple variants. |
| 33 | - PPT narration assets must be PowerPoint-reliable audio: `m4a` (AAC), `mp3`, or `wav`. The built-in TTS path defaults to `mp3`; provider formats such as `pcm`, `opus`, or `flac` must be transcoded before embedding. |
| 34 | - PowerPoint recorded narration export requires `ffprobe` so slide timings can be written from actual audio duration. |
| 35 | - Optional automatic video export requires Windows PowerPoint 2016+ and runs |
| 36 | through `powerpoint_video.py`; the command waits for PowerPoint's native |
| 37 | encoder to finish before returning. |
| 38 | - Optional slideshow capture is an explicit manual Windows PowerPoint handoff; |
| 39 | it is never an automatic fallback or project dependency. |
| 40 | - macOS PowerPoint may export MP4/MOV manually, but it has no equivalent |
| 41 | `CreateVideo` automation contract and its movie export does not preserve |
| 42 | animation effects. Do not replace the missing API with UI scripting. |
| 43 | - Optional post-export video calibration requires `ffmpeg` plus `numpy`; it runs only after a finished PowerPoint video is supplied or created. |
| 44 | - Direct MP4 delivery with resolved transition/object-animation sound cues also |
| 45 | requires `ffmpeg` plus `numpy`. It renders an independent SFX stem, mixes it |
| 46 | after native PowerPoint export, and validates the actual mixed audio track. |
| 47 | - High-quality cloud mode: provider API key is set before use: |
| 48 | - ElevenLabs: `ELEVENLABS_API_KEY` |
| 49 | - MiniMax: `MINIMAX_API_KEY` |
| 50 | - Qwen: `QWEN_API_KEY` or `DASHSCOPE_API_KEY` |
| 51 | - CosyVoice: `COSYVOICE_API_KEY` or `DASHSCOPE_API_KEY` |
| 52 | - Keys may live in the current process environment or the first `.env` found in this order: current working directory, skill directory (e.g. `~/.agents/skills/ppt-master/.env`), clone repo root, `~/.ppt-master/.env` |
| 53 | - The deck is in a single dominant language (mixed-language decks: pick the dominant one — the AI uses judgment, not a heuristic). |
| 54 | |
| 55 | If per-slide notes are missing, recover through the owning route. Generate |
| 56 | PPTX returns to its enabled notes branch and then runs |
| 57 | `total_md_split.py <project_path>`; Enhance Native PPTX returns to |
| 58 | `native-enhance-pptx` Step 6 and writes numeric notes directly. Never run the |
| 59 | Generate splitter against a Native Enhance project. |
| 60 | |
| 61 | --- |
| 62 | |
| 63 | ## Step 1: Determine the deck's language |
| 64 | |
| 65 | The AI already knows the deck's language from writing the notes. No detection script needed. |
| 66 | |
| 67 | - Identify the primary language from the notes content: `zh` / `en` / `ja` / `ko` / etc. |
| 68 | - For mixed-language decks (e.g. Chinese with English technical terms), pick the language the audience will hear most of. |
| 69 | - For Chinese specifically: pick the locale based on context — `zh-CN` (mainland mandarin, default), `zh-TW` (Taiwanese mandarin), or `zh-HK` (Cantonese). Default Generate may ask when context is unclear; Quick chooses the best supported default and continues. |
| 70 | |
| 71 | --- |
| 72 | |
| 73 | ## Step 2: Choose audio backend and pull the voice catalog |
| 74 | |
| 75 | Default to **edge** unless the user explicitly asks for a cloud provider / higher-quality cloud narration / a cloned voice. |
| 76 | |
| 77 | **edge backend**: |
| 78 | |
| 79 | ```bash |
| 80 | python3 skills/ppt-master/scripts/notes_to_audio.py --list-voices --locale <locale> |
| 81 | ``` |
| 82 | |
| 83 | **ElevenLabs backend**: |
| 84 | |
| 85 | ```bash |
| 86 | python3 skills/ppt-master/scripts/notes_to_audio.py --provider elevenlabs --list-voices |
| 87 | ``` |
| 88 | |
| 89 | **Cloud providers using explicit voice IDs/names**: |
| 90 | |
| 91 | ```bash |
| 92 | python3 skills/ppt-master/scripts/notes_to_audio.py --provider minimax --list-voices |
| 93 | python3 skills/ppt-master/scripts/notes_to_audio.py --provider qwen --list-voices |
| 94 | python3 skills/ppt-master/scripts/notes_to_audio.py --provider cosyvoice --list-voices |
| 95 | ``` |
| 96 | |
| 97 | The output is a flat list of all available voices for the selected provider. From this list, the AI picks **3–6 candidates** to recommend, applying these rules: |
| 98 | |
| 99 | - **Cover both genders** when both exist for the locale. |
| 100 | - **For edge**: prefer `COMMON_VOICES`-listed voices (curated set inside `notes_to_audio.py`) when the locale has them — they are battle-tested. |
| 101 | - **For ElevenLabs**: prefer voices already present in the user's account; if the user provides a specific `voice_id`, do not override it. |
| 102 | - **For MiniMax / Qwen / CosyVoice**: if the user provides a cloned `voice_id`, use it directly. Do not attempt voice cloning inside this narration stage. |
| 103 | - **For CosyVoice subtitles**: use a cloned voice from a supported v3.5/v3/v2 model or a system voice marked timestamp-supported. Model and voice families must match. Use `--cosyvoice-audio-only` only when the user accepts no page-local SRT. |
| 104 | - **Match the deck's tone** — pick the strongest recommendation based on style: |
| 105 | - Chinese consultant / data-driven / financial-report deck → a steady male voice (e.g. `zh-CN-YunjianNeural`) or a clear female voice (e.g. `zh-CN-XiaoxiaoNeural`) |
| 106 | - Chinese general / teaching / product-introduction deck → a bright female or young male voice (e.g. `zh-CN-XiaoyiNeural` / `zh-CN-YunxiNeural`) |
| 107 | - Chinese launch event / broadcast deck → a broadcast-toned male voice (e.g. `zh-CN-YunyangNeural`) |
| 108 | - English consultant deck → `en-US-GuyNeural` (steady) or `en-US-JennyNeural` (clear) |
| 109 | - Japanese / Korean → pick from `ja-JP-*` / `ko-KR-*` neural voices, mark gender + tone |
| 110 | |
| 111 | For each candidate, write a **one-line description in the user's chat language** covering: gender · tone · best-fit scenario. For cloud providers, include the voice name/ID exactly as it must be passed to `--voice-id`. |
| 112 | |
| 113 | --- |
| 114 | |
| 115 | ## Step 3: Resolve generation settings |
| 116 | |
| 117 | **Quick exception**: do not pause. Apply explicit user values, then resolve |
| 118 | unspecified provider, voice, rate, and embed choices from the recommended-value |
| 119 | rules below. Keep video off unless the caller selected direct video; then embed |
| 120 | the narrated PPTX and continue to native video only when |
| 121 | `powerpoint_video.py --check` succeeds. If final resolved motion contains sound |
| 122 | cues, continue automatically through the post-export mix without another |
| 123 | question. An explicit slideshow-capture request instead stops at the |
| 124 | capture-ready narrated PPTX until the user supplies the recorded MP4; it never |
| 125 | silently switches to native export. Require a timestamp-capable provider only |
| 126 | when narration-cue sync or subtitle delivery needs page-local SRT; on the |
| 127 | native-export branch, audio-only narration can still calibrate the sound mix |
| 128 | from its complete per-page tracks. |
| 129 | |
| 130 | **Default / Enhance Native — one-shot interaction (mandatory)**: |
| 131 | |
| 132 | For Default or Enhance Native, send one message that resolves all five configuration decisions and recommends each value. Before offering automatic video export, run `python3 skills/ppt-master/scripts/powerpoint_video.py --check`; do not present an unavailable local capability as executable. Do NOT split into multiple rounds. |
| 133 | An explicit slideshow-capture choice does not run this availability check; it |
| 134 | uses the manual Windows playback handoff below. |
| 135 | |
| 136 | **Cloned-voice fast path**: if the user mentioned a cloned voice / 克隆音色 / 复刻音色 / "my own voice" along with a `voice_id`, skip the voice-recommendation list — set the named provider (`elevenlabs` / `minimax` / `qwen` / `cosyvoice`) and pin that `voice_id`. Quick applies its exception above; Default and Enhance Native confirm only rate + embed + video. |
| 137 | |
| 138 | **Message template** (Chinese; translate to user's chat language if different). “Embed” means caller-specific integration: SVG re-export for Generate PPTX, or native OOXML application for Enhance Native PPTX. |
| 139 | |
| 140 | > 检测到 notes 主语言为 **<语言>**(locale: `<locale>`)。基于 deck 调性(<风格>),我推荐以下配置: |
| 141 | > |
| 142 | > **生成模式**:⭐ 推荐 `<edge|elevenlabs|minimax|qwen|cosyvoice>`(理由:<一句话,如"无需配置,稳定生成"或"用户要求高质量云端音色">)。 |
| 143 | > |
| 144 | > **音色**: |
| 145 | > - **[1] <ShortName>** — <性别·调性·适用场景> ⭐ **推荐** |
| 146 | > - [2] <ShortName> — <性别·调性·适用场景> |
| 147 | > - [3] <ShortName> — <性别·调性·适用场景> |
| 148 | > - [4] <ShortName> — <性别·调性·适用场景> |
| 149 | > - [5] <ShortName> — <性别·调性·适用场景> |
| 150 | > - 也可直接输入清单中的其他 ShortName。 |
| 151 | > |
| 152 | > **语速/风格参数**:⭐ 推荐 `<rate or provider defaults>`(理由:<一句话,如"页均 2–3 句,正常语速听感最稳"或"ElevenLabs 默认 voice settings 保留音色原始表现最稳">)。 |
| 153 | > |
| 154 | > **生成完是否重新导出嵌入音频的 PPTX**:⭐ 推荐 **是**(一次到位,自动按音频时长设页面停留)。 |
| 155 | > |
| 156 | > **带音频 PPTX 完成后是否继续导出视频**:⭐ 推荐 **原生编码**(本机 Windows PowerPoint 2016+ 可用时)。需要录下实际放映声音时可选 **实时放映录制**。 |
| 157 | > |
| 158 | > 直接回"好"用全部推荐值,或告诉我想改的部分(如"音色 2,语速 -5%"或"用 MiniMax 的 voice_id xxx")。 |
| 159 | |
| 160 | **Recommended-value rules**: |
| 161 | - **Generation mode**: default `edge`; follow the user's choice when they name a cloud provider / voice ID. Do not recommend Qwen when page-local SRT, subtitle animation, or video subtitles are needed; if the user insists, state that only audio is delivered and skip the SRT step. |
| 162 | - **Voice**: pick the Step 2 candidate that fits the deck's tone best. |
| 163 | - **Rate**: edge defaults to `+0%`; recommend `-5%` for dense notes (>4 long sentences per page) and `+5%` for short, tight notes; going outside this range needs a stated reason. Cloud providers keep provider defaults unless the user explicitly asks to change speed or style. |
| 164 | - **Embed**: recommend yes by default, unless the user already has a customized PPTX they do not want overwritten. |
| 165 | - **Video**: recommend native encoding when `powerpoint_video.py --check` succeeds; use slideshow capture only on an explicit user choice. When automation is unavailable, deliver the narrated PPTX; never silently switch to screen recording or a third-party renderer. |
| 166 | |
| 167 | --- |
| 168 | |
| 169 | ## Step 4: Execute (no further interaction) |
| 170 | |
| 171 | **Blocking notes preflight**: `notes_to_audio.py` resolves the complete notes |
| 172 | roster from `svg_output/*.svg` on Generate projects or |
| 173 | `analysis/slide_index.json` on Native Enhance projects. Before any TTS request, |
| 174 | every expected note must exist, be readable, and contain spoken text. Exit code |
| 175 | `2` returns the caller to its notes-generation step; never continue with partial |
| 176 | audio generation. |
| 177 | |
| 178 | Run sequentially — do NOT bundle: |
| 179 | |
| 180 | ```bash |
| 181 | # 1A. Generate audio with edge (default) |
| 182 | python3 skills/ppt-master/scripts/notes_to_audio.py <project_path> \ |
| 183 | --voice <chosen-ShortName> --rate <chosen-rate> |
| 184 | |
| 185 | # 1B. Or generate audio/SRT pairs with ElevenLabs |
| 186 | python3 skills/ppt-master/scripts/notes_to_audio.py <project_path> \ |
| 187 | --provider elevenlabs --voice-id <chosen-voice-id> \ |
| 188 | --elevenlabs-model eleven_multilingual_v2 |
| 189 | |
| 190 | # 1C. Or generate audio with MiniMax |
| 191 | # Defaults to the China endpoint; set MINIMAX_TTS_BASE_URL=https://api.minimax.io/v1/t2a_v2 for overseas access. |
| 192 | python3 skills/ppt-master/scripts/notes_to_audio.py <project_path> \ |
| 193 | --provider minimax --voice-id <chosen-voice-id> \ |
| 194 | --minimax-model speech-2.8-hd |
| 195 | |
| 196 | # 1D. Or generate audio only with Qwen TTS (the API returns no timestamps) |
| 197 | python3 skills/ppt-master/scripts/notes_to_audio.py <project_path> \ |
| 198 | --provider qwen --voice-id <chosen-voice> \ |
| 199 | --qwen-model qwen3-tts-flash --qwen-language-type Chinese |
| 200 | |
| 201 | # 1E. Or generate audio/SRT pairs with a timestamp-capable CosyVoice voice |
| 202 | python3 skills/ppt-master/scripts/notes_to_audio.py <project_path> \ |
| 203 | --provider cosyvoice --voice-id <chosen-voice> \ |
| 204 | --cosyvoice-model cosyvoice-v3-flash |
| 205 | |
| 206 | # 2A. Only when narration-cue sync is selected and page SRT + animations.json |
| 207 | # exist, author or refresh narration_timing.json |
| 208 | # by matching SVG group semantics to SRT topics, then derive the narrated |
| 209 | # sidecar. Reuse current SVG semantics when complete; otherwise read only |
| 210 | # the missing or stale svg_output pages. |
| 211 | python3 skills/ppt-master/scripts/narration_sync.py animations <project_path> \ |
| 212 | --narration-start-floor 0.8 --narration-padding 0.5 --force |
| 213 | |
| 214 | # 2B. Re-export with audio embedded |
| 215 | # Use the base export's [REPORT] path to preserve source-bound deck motion. |
| 216 | # Quick Generate adds --quick-generate --with-notes to every re-export below. |
| 217 | # For the native-export mix branch when final motion has sound cues, also |
| 218 | # pass --conversion-trace <final_narrated_trace>. Explicit slideshow capture |
| 219 | # does not require that trace for sound delivery. |
| 220 | python3 skills/ppt-master/scripts/svg_to_pptx.py <project_path> \ |
| 221 | --recorded-narration audio \ |
| 222 | --narration-start-floor 0.8 --narration-padding 0.5 \ |
| 223 | --inherit-motion-from "<base_postflight_report>" |
| 224 | |
| 225 | # Optional: use the canonical presentation animation instead |
| 226 | python3 skills/ppt-master/scripts/svg_to_pptx.py <project_path> \ |
| 227 | --recorded-narration audio \ |
| 228 | --narration-start-floor 0.8 --narration-padding 0.5 \ |
| 229 | --animation-config animations.json \ |
| 230 | --inherit-motion-from "<base_postflight_report>" |
| 231 | |
| 232 | # Optional: export narration with no object or page-transition animation |
| 233 | python3 skills/ppt-master/scripts/svg_to_pptx.py <project_path> \ |
| 234 | --recorded-narration audio \ |
| 235 | --narration-start-floor 0.8 --narration-padding 0.5 \ |
| 236 | --no-animations |
| 237 | |
| 238 | # 2C. Only when page-local SRT exists, merge it against timing values read |
| 239 | # from the final PPTX |
| 240 | python3 skills/ppt-master/scripts/narration_sync.py subtitles <project_path> \ |
| 241 | --pptx <final_narrated_pptx> --force |
| 242 | |
| 243 | # 2D. Optional: export the raw video through installed Windows PowerPoint |
| 244 | # and wait for completion |
| 245 | python3 skills/ppt-master/scripts/powerpoint_video.py \ |
| 246 | <final_narrated_pptx> -o <raw_powerpoint_video.mp4> |
| 247 | |
| 248 | # 2E. Only when final resolved motion has sound cues and direct MP4 delivery is |
| 249 | # selected, derive the exact embedded sounds from the final narrated PPTX, |
| 250 | # calibrate cue times against raw video narration, and publish the verified |
| 251 | # SFX stem, mixed MP4, and report. Defaults are about 35% for transitions, |
| 252 | # 25% for object cues, and a -1 dBFS limiter. |
| 253 | python3 skills/ppt-master/scripts/video_sound_mix.py <project_path> \ |
| 254 | --pptx <final_narrated_pptx> \ |
| 255 | --trace <final_narrated_trace> \ |
| 256 | --video <raw_powerpoint_video.mp4> \ |
| 257 | -o <final_mixed_video.mp4> \ |
| 258 | --stem-output <final_sfx_stem.wav> \ |
| 259 | --report-output <sound_mix_report.json> --force |
| 260 | |
| 261 | # 2F. Only when page-local SRT exists, align the frozen narration text against |
| 262 | # the final delivery video: mixed when 2E ran, captured when the explicit |
| 263 | # slideshow-capture handoff returned an MP4, otherwise the raw video. |
| 264 | python3 skills/ppt-master/scripts/video_subtitles.py <project_path> \ |
| 265 | --video <final_delivery_video.mp4> --language <language> --force |
| 266 | ``` |
| 267 | |
| 268 | **Explicit slideshow capture**: desktop Windows PowerPoint plays the final |
| 269 | narrated PPTX full-screen from the beginning with automatic, click-free timing; |
| 270 | capture only the deck frame and one application/system-audio source, with mic, |
| 271 | UI, pointer, and notifications absent. Trim short head/tail handles. Human-check |
| 272 | streams, narration, every cue once, complete motion, and no dropped frames. The |
| 273 | capture has no machine cue receipt and must never enter `video_sound_mix.py`. |
| 274 | If the host cannot capture, report only the capture-ready PPTX handoff. Align |
| 275 | page SRT against an accepted capture and append one compact `workflow_log.py` |
| 276 | note. |
| 277 | |
| 278 | **Default — bounded Edge concurrency (may override)**: Generate up to three |
| 279 | slide-level audio/SRT pairs concurrently. Use `--concurrency <N>` to tune the |
| 280 | Edge path or `--concurrency 1` for serial troubleshooting. Cloud providers |
| 281 | remain serial. |
| 282 | |
| 283 | If `notes_to_audio.py` errors with a missing dependency or missing provider API key, fix the prerequisite and re-run — do NOT swallow the error. |
| 284 | |
| 285 | The edge command writes each MP3 and its internal page SRT from the same `edge-tts` stream. SRT cues use the service's `WordBoundary` timing: sentence-ending punctuation always closes a cue; text over the default 20-visible-character limit first splits at commas, semicolons, or colons, then at the nearest word boundary. Override the limit with `--subtitle-max-chars`. Adjacent timing overlap up to 100 ms is tolerated by moving the later cue start to the previous cue end; larger overlap fails instead of silently distorting timing. Each SRT uses a page-local timeline whose origin is `00:00:00,000`, including any leading silence before the first cue. |
| 286 | |
| 287 | MiniMax reads word timing from its synchronous subtitle file. ElevenLabs uses `/with-timestamps` and original-text character alignment. CosyVoice enables HTTP streaming plus `word_timestamp_enabled`, then uses the final audio URL and word timing from that synthesis; unsupported model/voice pairs fail without replacing the prior pair unless `--cosyvoice-audio-only` was explicit. Qwen exposes no timing, so it remains audio-only and this stage never estimates SRT timing. |
| 288 | |
| 289 | Provider-timed paths share punctuation-first, `--subtitle-max-chars`-bounded regrouping, exact-text validation, and rollback-safe pair publication. See [`docs/audio-narration.md`](../../../../docs/audio-narration.md) for current model and audio-parameter recommendations. |
| 290 | |
| 291 | Before generation starts, `notes_to_audio.py` removes stale `audio/manifest.json` and `audio/total.srt`; an incomplete run therefore cannot claim the previous set's provenance or merged timeline. A successful audio-only provider run also removes same-stem stale SRT files. The new manifest is published atomically only after the complete page roster succeeds. |
| 292 | |
| 293 | **Mandatory when narration-cue sync is selected — semantic animation context**: Before writing or refreshing `<project_path>/narration_timing.json`, determine whether the active context already contains the current top-level SVG group IDs and visible group-content semantics for every affected page. Reuse that context without rereading SVG when it is complete and still matches the current `svg_output/`. If any page is missing, stale, or represented only by group IDs/order without content meaning, read only that page's SVG as a read-only source and extract the missing group semantics. Always combine those semantics with the page SRT topics/timestamps and `animations.json`; group order alone is not a semantic narration mapping. |
| 294 | |
| 295 | > Narration-cue sync with `animations.json` requires `narration_timing.json`. |
| 296 | > Narration-independent custom motion instead passes `--animation-config animations.json` |
| 297 | > and makes no object-sync claim. Explicit `--no-animations` |
| 298 | > bypasses both. Without a timing sidecar, `narration_sync.py animations` maps |
| 299 | > groups **positionally** (group N → cue N) and warns when later objects may |
| 300 | > reveal during an earlier topic. Treat that warning as required repair: author |
| 301 | > the semantic plan and re-derive. |
| 302 | |
| 303 | **Narration animation ownership**: When narration-cue sync is selected, `animations.json` remains read-only. The audio stage deep-copies it to `narration_animations.json`, preserves transitions, effects, durations, order, and explicit `effect: none`, then changes only the derived trigger/delay values needed for click-free narration playback. The authored `narration_timing.json` maps each animated content group—not each effect row—to the SRT cue that speaks about that content. For `effects[]`, the cue anchors the group's first active row; later rows keep global order and their relative delay. The command may still read an affected SVG page to resolve structural group order when a sparse sidecar cannot identify every effective group; this structural fallback does not replace the semantic-context step and never edits SVG, notes, or `animations.json`. Unmatched groups keep their canonical relative delay. |
| 304 | |
| 305 | **Title timing handoff when canonical animation exists**: preserve the title reveal decision already made by the custom-animation pass. Assign a title group to an SRT cue only when the user's request or the active motion plan explicitly chose `narration-cued`; otherwise leave its `cue` omitted in `narration_timing.json` so it keeps the canonical relative delay from `animations.json`. Do not infer `narration-cued` merely because speaker notes mention the title. |
| 306 | |
| 307 | **Narrated export animation selection**: |
| 308 | |
| 309 | | Sidecar state | Behavior | |
| 310 | |---|---| |
| 311 | | `narration_animations.json` exists and narration-cue sync is selected | Use it | |
| 312 | | Only canonical `animations.json` exists and narration-cue sync is selected | Block until narration synchronization creates the derived sidecar | |
| 313 | | Canonical `animations.json` exists and motion is narration-independent, whether or not a derived sidecar also exists | Pass `--animation-config animations.json`; do not claim object sync | |
| 314 | | Both are absent | Create no sidecar; inherit the base report's deck motion | |
| 315 | |
| 316 | Generate passes the base report through `--inherit-motion-from`: inherited |
| 317 | `-a none` preserves explicit objects-off, while final Stage-2 `false` does not. |
| 318 | Only explicit all-motion-off uses `--no-animations`. Invalid reports block; |
| 319 | page-start lead-in, audio duration, and page-tail padding own final advance. |
| 320 | |
| 321 | **Narration pacing controls**: page-front and page-tail timing are independent, |
| 322 | optional parameters. Unless the user supplies values, use |
| 323 | `narration_start_floor=0.8` seconds and `narration_padding=0.5` seconds without |
| 324 | adding a confirmation question. For a destination-page transition of `T` |
| 325 | seconds, the post-transition lead-in is |
| 326 | `max(0, narration_start_floor - T)`: narration never begins during the |
| 327 | transition, while a longer transition is not stretched. Apply the same |
| 328 | lead-in to embedded narration, cue-bound object animation, subtitle offsets, |
| 329 | and slide advance. Uncued title or decorative animation keeps its canonical |
| 330 | relative timing. Setting the start floor to `0` means narration begins as soon |
| 331 | as the transition completes; it does not bypass the transition. |
| 332 | |
| 333 | When canonical custom animation is synchronized, |
| 334 | `<project_path>/narration_timing.json` is the explicit semantic mapping for |
| 335 | narrated object animation. It is fingerprinted to the ordered SRT set; `cue` |
| 336 | is the 1-based subtitle cue, and omitted `cue` keeps that group's canonical |
| 337 | relative delay. Reuse a complete current mapping when its fingerprint and SVG |
| 338 | group semantics remain valid; rebuild only affected pages when either input |
| 339 | changed. |
| 340 | |
| 341 | Get the exact fingerprint value with: |
| 342 | |
| 343 | ```bash |
| 344 | python3 skills/ppt-master/scripts/narration_sync.py fingerprint <project_path> |
| 345 | ``` |
| 346 | |
| 347 | ```json |
| 348 | { |
| 349 | "version": 1, |
| 350 | "srt_sha256": "<sha256 of the ordered page-local SRT set>", |
| 351 | "narration_start_floor": 0.8, |
| 352 | "narration_padding": 0.5, |
| 353 | "slides": { |
| 354 | "01_title": { |
| 355 | "groups": [ |
| 356 | { "id": "page-title", "cue": 1 }, |
| 357 | { "id": "supporting-visual" } |
| 358 | ] |
| 359 | } |
| 360 | } |
| 361 | } |
| 362 | ``` |
| 363 | |
| 364 | `narration_sync.py subtitles` may still write `<project_path>/audio/total.srt` as a PPTX-timeline diagnostic. It is not the delivery subtitle for a finished video. |
| 365 | |
| 366 | When video export was selected, `powerpoint_video.py` opens the final narrated |
| 367 | PPTX through local Windows PowerPoint, requests its native video encoder with |
| 368 | recorded timings and narrations enabled, and polls `CreateVideoStatus` until the |
| 369 | MP4 succeeds, fails, or times out. The interface is synchronous to its caller |
| 370 | even though PowerPoint performs encoding asynchronously. It preserves the |
| 371 | native visual-animation and narration path rather than re-rendering the deck, |
| 372 | but does not reliably write transition or object-animation sounds into the MP4 |
| 373 | audio track. This is the default automated video path; an explicitly selected |
| 374 | slideshow capture bypasses `CreateVideo` but still uses desktop PowerPoint as |
| 375 | the real-time renderer and audio player. |
| 376 | |
| 377 | If native video export fails, keep the narrated PPTX as a successful upstream |
| 378 | artifact and report the video failure separately. Do not regenerate audio or |
| 379 | the PPTX unless their own validation failed. |
| 380 | |
| 381 | On the native-export path, when the final narrated trace and PPTX contain sound |
| 382 | cues, treat the PowerPoint MP4 as a raw intermediate. `video_sound_mix.py` |
| 383 | cross-checks that trace against the final PPTX read-back, extracts the exact |
| 384 | embedded sound relationships, calibrates every page against the raw video's |
| 385 | narration, renders a float SFX stem, and mixes it with narration at unity gain. |
| 386 | Transition cues default to about 35%, object cues to about 25%; `amix` |
| 387 | normalization and ducking remain off, and a -1 dBFS peak limiter follows the |
| 388 | mix. The receipt must prove a non-silent stem, preserved video-stream hash, |
| 389 | changed and present final audio, duration parity, non-clipping true peak, and |
| 390 | correlation between the added final-audio component and the stem. A valid |
| 391 | `animations.json` or OOXML package alone is not MP4 audio acceptance. |
| 392 | |
| 393 | After the final delivery MP4 exists, `video_subtitles.py` takes the exact |
| 394 | narration text frozen in the page SRT set and force-aligns it against that |
| 395 | finished video's actual audio track with `stable-ts`. Use the mixed MP4 when |
| 396 | sound mixing ran, the accepted capture when slideshow recording ran, otherwise |
| 397 | the raw PowerPoint MP4. Long delivery cues may be split for display at this |
| 398 | final stage. This writes a same-stem external SRT without changing the MP4, |
| 399 | notes, page SRT, or animation files. |
| 400 | |
| 401 | This stage keeps subtitles as external SRT files and never burns them in. |
| 402 | Automatic export is an optional Windows PowerPoint integration. When it is |
| 403 | unavailable, stop after the narrated PPTX unless explicit capture is selected; |
| 404 | that handoff remains incomplete until a real capture is accepted. |
| 405 | |
| 406 | **Caller integration**: |
| 407 | |
| 408 | | Caller | After audio generation | |
| 409 | |---|---| |
| 410 | | Generate PPTX | Derive narration-cued motion when selected; otherwise pass canonical motion, inherit base motion, or use explicit all-motion-off. Export with `--recorded-narration audio`; Quick also passes `--quick-generate --with-notes`. Native video uses conversion trace plus raw export and cue mix as required. Explicit capture returns the narrated PPTX for the handoff above, skips trace-only sound work and mixing, then aligns subtitles against the accepted capture. | |
| 411 | | Enhance Native PPTX | Return to [`native-enhance-pptx`](../native-enhance-pptx.md) Step 9. Native video passes its final PPTX to `powerpoint_video.py`; explicit capture uses the same handoff above and skips mixing. | |
| 412 | |
| 413 | For Qwen or explicit CosyVoice audio-only mode, embed/export the audio normally |
| 414 | but skip `narration_timing.json`, `narration_sync.py animations`, SRT merge, and |
| 415 | final-video subtitle alignment. Pass canonical narration-independent custom |
| 416 | motion explicitly when present. On the native-export branch, a direct-MP4 sound |
| 417 | mix may still run because page audio, not SRT, supplies its correlation |
| 418 | template. Never present missing subtitle artifacts or object sync as generated. |
| 419 | |
| 420 | For Generate PPTX, `--recorded-narration audio` prepares PowerPoint's recorded timings and narrations: every slide must have a matching supported audio file, every duration must be readable by `ffprobe`, and object animations must not use `--animation-trigger on-click`. Use `after-previous` or `with-previous` for narrated/video export. Narration changes the slide-advance layer only: the resolved page-transition effect remains unchanged, `-t none` remains visually transition-free, and narration advance disables click while using page-start lead-in plus audio duration plus page-tail padding. The re-export is saved as `exports/<project_name>_<timestamp>_narrated.pptx`, telling it apart from silent exports. |
| 421 | |
| 422 | **Narrated SVG export**: use the default text-flow mode. It keeps authored line breaks in one editable, no-wrap text frame; narration does not require per-line text frames. |
| 423 | |
| 424 | --- |
| 425 | |
| 426 | ## Step 5: Completion report |
| 427 | |
| 428 | Output one summary block listing: |
| 429 | |
| 430 | - Number of audio files generated and their location (`<project_path>/audio/*`). |
| 431 | - For provider-timed subtitles, number of matching page-local SRT files and their location (`<project_path>/audio/*`); for Qwen or explicit CosyVoice audio-only mode, report that no page-local SRT was generated. |
| 432 | - Narration provider/model plus the `<project_path>/audio/manifest.json` provenance path. |
| 433 | - For narrated object animation, whether current SVG semantics were reused or which missing/stale pages were reread, plus semantic mapping coverage and fallback count. |
| 434 | - For Generate PPTX, report derived narration animation coverage/path when cue sync ran, the canonical config path for narration-independent custom motion, or inherited/all-motion-off state. |
| 435 | - When native video export was selected, the raw PowerPoint MP4 path/status. |
| 436 | When resolved cues triggered sound mixing, also report the final mixed MP4, |
| 437 | SFX stem, cue count, and `video_sound_mix.py` receipt; otherwise identify the |
| 438 | raw MP4 as final. |
| 439 | - For slideshow capture, report the capture-ready PPTX handoff or accepted MP4 |
| 440 | plus system-audio and human picture/narration/all-cue status; never report a |
| 441 | mix receipt. |
| 442 | - When page-local SRT was merged, the PPTX-timeline `audio/total.srt` path. |
| 443 | - When final-video subtitle alignment ran, the aligned delivery SRT path and |
| 444 | whether its source was the mixed, captured, or raw final video; |
| 445 | otherwise do not claim a video-aligned subtitle. |
| 446 | - The provider, voice, and rate/settings actually used. |
| 447 | - The caller-owned integration result: narrated SVG export path, enhanced native PPTX path, or “audio only”. |
| 448 | - For Generate PPTX when embedding was skipped, one-line hint: `python3 skills/ppt-master/scripts/svg_to_pptx.py <project_path> --recorded-narration audio`. |
| 449 |