返回 ppt-master
svg-pipeline.md
根目录 / skills / ppt-master / scripts / docs / svg-pipeline.md
1 # SVG Pipeline Tools
2
3 > **Maintenance boundary**: post-processing modules serve both the on-disk
4 > `svg_final/` preview and in-memory native PPTX conversion. Check both
5 > consumers before changing or removing a step.
6
7 These tools cover post-processing, SVG validation, speaker notes, recorded narration, and PPTX export.
8
9 The supported delivery contract has one PPTX path: `svg_output/` → the project SVG-to-DrawingML converter → native PPTX. The mandatory `finalize_svg.py` step separately creates self-contained `svg_final/` visual previews, which may be opened directly or inserted into PowerPoint as SVG pictures. There is no SVG-image PPTX output, and PowerPoint's manual Convert-to-Shape operation is unsupported.
10
11 ## `svg_authoring_view.py`
12
13 Create a lightweight editable authoring IR bundle from one PPTX-imported SVG or
14 a directory of imported SVGs:
15
16 ```bash
17 python3 scripts/svg_authoring_view.py <svg-file-or-directory> -o <output-dir> \
18 --projection-kind layered
19 ```
20
21 The operation is non-destructive and refuses existing output files unless
22 `--force` is explicit. It never writes back to the source SVG. The JSON report
23 on stdout records original/projected byte counts and removals by category. The
24 output directory contains the editable SVGs, one model-readable
25 `authoring_summary.json`, and one tool-only `authoring_manifest.json`.
26
27 The projected copy:
28
29 - removes embedded `txbody` metadata;
30 - removes hidden native geometry carriers while retaining and unwrapping their
31 visible preview geometry;
32 - removes source-object identity/style/hash attributes that are only useful to
33 an exact import round trip;
34 - keeps visible paths, text, images, stable ids, Master/Layout root markers,
35 selected native-shape intent, and a document-local `data-pptx-source-ref` on
36 each imported logical object;
37 - rewrites relative local asset references for the projection's new location;
38 - compacts imported model-facing frames and safe transform page coordinates to
39 at most two decimals.
40
41 The summary stores the current SVG roster plus compact per-file canvas, size,
42 text, image, vector, placeholder, icon, and source-ref counts. Models read the
43 summary and editable SVGs; they do not read the machine manifest. The manifest
44 stores relative source/authoring filenames, source and initial authoring hashes,
45 and source element paths. It deliberately does not copy the opaque payload.
46 The authoring bundle is the editable source for template creation; the complete
47 imported SVG remains immutable native-payload backing. Final
48 `templates/*.svg` files are materialized and validated from that pair. The IR
49 directory itself is not a supported direct input to `svg_to_pptx.py`.
50
51 Regenerate the summary after direct edits that do not pass through one of the
52 in-place normalization tools:
53
54 ```bash
55 python3 scripts/svg_authoring_view.py <authoring-dir> --refresh-summary
56 ```
57
58 This projection is separate from canonical preset authoring. New project SVGs
59 and project-owned templates use the compact authored form: one atomic
60 `<g data-pptx-authoring="preset">` owns the preset intent and base paint, with
61 the registry-generated visible `<path>` layers as direct children. Quality
62 check and export rerender the locked registry to validate that group, so the
63 compact form has no hidden carrier, preview wrapper, or serialized preview
64 fingerprint. `pptx_to_svg.py` continues to emit the expanded carrier/preview
65 evidence required for import and round-trip decisions. The normative boundary
66 is owned by [`shared-standards-core.md`](../../references/shared-standards-core.md) §1.5, with
67 authoring guidance in
68 [`native-shape-authoring.md`](../../references/native-shape-authoring.md).
69
70 ## Shape Boolean maintenance smoke
71
72 Run this manual smoke from the repository root after changing
73 `shape_boolean_svg.py`, preset geometry, path conversion, or custom-geometry
74 import/export. It uses only a gitignored `projects/_smoke_*` workspace and the
75 inline-smoke convention from [`code-style.md`](../../../../docs/rules/code-style.md)
76 §11; do not turn it into a test file or example deck.
77
78 ```bash
79 python3 - <<'PY'
80 import re
81 import subprocess
82 import sys
83 import tempfile
84 import zipfile
85 from pathlib import Path
86 from xml.etree import ElementTree as ET
87
88 import pathops
89
90 project = Path(tempfile.mkdtemp(prefix="_smoke_shape_boolean_", dir="projects"))
91 scripts = Path("skills/ppt-master/scripts")
92 svg_output = project / "svg_output"
93 svg_output.mkdir()
94 (project / "spec_lock.md").write_text(
95 """<!-- ppt-master-schema: spec-lock/v1 -->
96 # Execution Lock
97
98 ## canvas
99 - viewBox: 0 0 1280 720
100 - format: ppt169
101 ## communication
102 - audience:
103 - objective:
104 - core_message:
105 ## mode
106 - mode: briefing
107 ## visual_style
108 - visual_style: Boolean maintenance smoke
109 ## colors
110 - bg: #FFFFFF
111 - primary: #2563EB
112 - accent: #F97316
113 - text: #0F172A
114 ## typography
115 - font_family: Arial, sans-serif
116 - title_family: Arial, sans-serif
117 - body_family: Arial, sans-serif
118 - title: 36
119 - body: 20
120 ## icons
121 - library: none
122 - inventory: none
123 ## page_rhythm
124 - P01: dense
125 ## pptx_structure
126 - mode: flat
127 ## forbidden
128 - Unsupported SVG constructs
129 """,
130 encoding="utf-8",
131 )
132
133
134 def run_tool(script, *args):
135 result = subprocess.run(
136 [sys.executable, str(scripts / script), *map(str, args)],
137 capture_output=True, text=True,
138 )
139 assert result.returncode == 0, result.stderr or result.stdout
140 return result.stdout.strip()
141
142 preset = run_tool(
143 "preset_shape_svg.py", "render", "rightArrow",
144 "--id", "preset-source", "--frame", "500", "120", "240", "120",
145 "--fill", "#2563EB", "--stroke", "none",
146 )
147 source = project / "operands.svg"
148 source.write_text(
149 f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720">
150 <defs><clipPath id="clip"><rect width="80" height="80"/></clipPath></defs>
151 <g transform="translate(100 80) scale(1.2)">
152 <rect id="body" x="40" y="40" width="400" height="240" rx="20"
153 fill="#2563EB" stroke="#0F172A" stroke-width="5"
154 stroke-dasharray="10 4"/>
155 <circle id="cutout" cx="240" cy="160" r="70" fill="#F97316"/>
156 <text id="text-cutout" x="240" y="235" text-anchor="middle"
157 font-family="sans-serif" font-size="180" font-weight="700">01</text>
158 <text id="missing-font" x="240" y="235"
159 font-family="Definitely Missing Font" font-size="80">X</text>
160 <text id="nested-text" x="240" y="235"
161 font-family="sans-serif" font-size="80"><tspan>X</tspan></text>
162 <path id="open" d="M 40 320 L 260 320 L 260 420" fill="#2563EB"/>
163 <rect id="clipped" x="40" y="320" width="160" height="100"
164 clip-path="url(#clip)" fill="#2563EB"/>
165 <path id="imported" d="M 240 320 H 400 V 420 H 240 Z"
166 data-pptx-geometry-kind="custom" fill="#2563EB"/>
167 <rect id="dashoffset" x="440" y="320" width="120" height="100"
168 fill="#2563EB" stroke="#0F172A" stroke-dashoffset="2"/>
169 <rect id="non-scaling" x="500" y="40" width="150" height="120"
170 fill="#2563EB" stroke="#0F172A" stroke-width="5"
171 stroke-dasharray="10 4" vector-effect="non-scaling-stroke"/>
172 <circle id="non-scaling-cut" cx="625" cy="100" r="45" fill="#F97316"/>
173 <rect id="far" x="800" y="320" width="100" height="80" fill="#2563EB"/>
174 </g>
175 {preset}
176 <circle id="preset-cut" cx="690" cy="180" r="52" fill="#F97316"/>
177 </svg>
178 """,
179 encoding="utf-8",
180 )
181
182 operations = [
183 ("union", "union", "preset-source", "preset-cut"),
184 ("combine", "combine", "body", "cutout"),
185 ("fragment", "fragment", "body", "cutout"),
186 ("intersect", "intersect", "body", "cutout"),
187 ("subtract", "subtract", "body", "cutout"),
188 ("text-subtract", "subtract", "body", "text-cutout"),
189 ]
190 expected_custom_shapes = 0
191 for index, (name, operation, first, second) in enumerate(operations, start=1):
192 fragment = run_tool(
193 "shape_boolean_svg.py", "render", source, "--operation", operation,
194 "--source", first, "--source", second, "--id", f"result-{name}",
195 )
196 paths = list(
197 ET.fromstring(
198 f'<svg xmlns="http://www.w3.org/2000/svg">{fragment}</svg>'
199 )
200 )
201 assert paths and all(path.tag.endswith("}path") for path in paths)
202 assert all(
203 token not in fragment
204 for token in ("clip-path=", "fill-rule=", "mask=", "transform=")
205 )
206 if operation == "fragment":
207 assert len(paths) > 1
208 assert [path.get("id") for path in paths] == [
209 f"result-{name}-{piece}"
210 for piece in range(1, len(paths) + 1)
211 ]
212 else:
213 assert len(paths) == 1
214 assert paths[0].get("id") == f"result-{name}"
215 if operation == "combine":
216 assert all(path.get("stroke-width") == "6" for path in paths)
217 assert all(path.get("stroke-dasharray") == "12 4.8" for path in paths)
218 if operation == "subtract":
219 assert (paths[0].get("d") or "").count("M ") >= 2
220
221 expected_custom_shapes += len(paths)
222 (svg_output / f"{index:02d}_{name}.svg").write_text(
223 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720" '
224 'data-pptx-page-role="content">'
225 '<rect x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>'
226 f"{fragment}</svg>\n",
227 encoding="utf-8",
228 )
229
230 non_scaling = ET.fromstring(
231 run_tool(
232 "shape_boolean_svg.py", "render", source, "--operation", "union",
233 "--source", "non-scaling", "--source", "non-scaling-cut",
234 "--id", "result-non-scaling",
235 )
236 )
237 assert non_scaling.get("stroke-width") == "5"
238 assert non_scaling.get("stroke-dasharray") == "10 4"
239 assert non_scaling.get("vector-effect") == "non-scaling-stroke"
240
241 rejections = [
242 ("union", "body", "open", "open subpath"),
243 ("union", "body", "clipped", "uses clip-path"),
244 ("union", "body", "imported", "PPTX import/round-trip metadata"),
245 ("union", "body", "dashoffset", "stroke-dashoffset"),
246 ("intersect", "body", "far", "produced no filled area"),
247 ("subtract", "body", "missing-font", "cannot resolve an installed font"),
248 ("subtract", "body", "nested-text", "child content is unsupported"),
249 ]
250 for operation, first, second, expected_error in rejections:
251 rejected = subprocess.run(
252 [
253 sys.executable,
254 str(scripts / "shape_boolean_svg.py"),
255 "render", str(source), "--operation", operation,
256 "--source", first, "--source", second,
257 "--id", f"reject-{second}",
258 ],
259 capture_output=True, text=True,
260 )
261 assert rejected.returncode != 0
262 assert expected_error in rejected.stderr, rejected.stderr
263
264 run_tool(
265 "svg_quality_checker.py", project,
266 "--quick-generate", "--format", "ppt169",
267 "--stage", "final", "--json",
268 )
269 pptx = project / "boolean-smoke.pptx"
270 run_tool("svg_to_pptx.py", project, "--quick-generate", "-o", pptx)
271 with zipfile.ZipFile(pptx) as archive:
272 slides = [
273 name
274 for name in archive.namelist()
275 if re.fullmatch(r"ppt/slides/slide\d+\.xml", name)
276 ]
277 custom_shapes = sum(
278 archive.read(name).count(b"<a:custGeom>")
279 for name in slides
280 )
281 assert len(slides) == len(operations), slides
282 assert custom_shapes == expected_custom_shapes
283
284 readback = project / "readback"
285 run_tool(
286 "pptx_to_svg.py", pptx, "-o", readback,
287 "--inheritance-mode", "flat", "--strict",
288 )
289 slides = sorted((readback / "svg").glob("slide_*.svg"))
290 readback_custom_shapes = sum(
291 slide.read_text(encoding="utf-8").count('data-pptx-custgeom="')
292 for slide in slides
293 )
294 assert len(slides) == len(operations), slides
295 assert readback_custom_shapes == expected_custom_shapes
296 print(
297 f"Shape Boolean smoke: passed "
298 f"({expected_custom_shapes} custom shapes; {project})"
299 )
300 PY
301 ```
302
303 The seven inline negative cases must return nonzero and match their expected
304 errors; every other command must pass. Open the printed
305 `boolean-smoke.pptx` path in PowerPoint: both shape-cut and text-cut Subtract
306 results must have real holes, and every Fragment sibling must remain separately
307 selectable.
308
309 ## `compact_svg_coordinates.py`
310
311 Compact safe model-facing page-space coordinates without rewriting unrelated
312 SVG formatting:
313
314 ```bash
315 python3 scripts/compact_svg_coordinates.py <svg-file-or-directory>
316 python3 scripts/compact_svg_coordinates.py <template-directory> \
317 --inplace --keep-native-frames
318 ```
319
320 The default run is a dry-run JSON report. `--inplace` atomically replaces only
321 changed SVG files. The shared create-template final pass uses
322 `--keep-native-frames`: it compacts `data-pptx-bounds`, translation values,
323 rotation centers, and
324 matrix `e/f`, while preserving canonical
325 authored-preset or inline native frames. `svg_authoring_view.py` separately
326 compacts imported model-facing frames because unchanged mirror refs can recover
327 their exact coordinates from immutable lossless backing.
328
329 The compactor never rounds path/points geometry, normalized crop or nested
330 `viewBox` ratios, gradient offsets, opacity, scale arguments, rotation angles,
331 or matrix `a/b/c/d` coefficients. Type A mirror materialization invokes the
332 same compactor before native-record externalization; `standard` and `fidelity`
333 use the shared final pass before template validation.
334
335 ## `extract_svg_assets.py`
336
337 Factor large vector subtrees out of lightweight authoring IR documents and
338 replace them with compact `<use data-icon>` references:
339
340 ```bash
341 python3 scripts/extract_svg_assets.py <layered_svg_dir> \
342 --icons-dir <icons_dir> --icon-namespace imported \
343 --inplace --id-prefix layered
344 python3 scripts/extract_svg_assets.py <flat_svg_dir> \
345 --icons-dir <icons_dir> --icon-namespace imported \
346 --reuse-inventory <layered_inventory.json> \
347 --inplace --id-prefix flat
348 ```
349
350 The first pass records a source fingerprint before namespacing each extracted
351 asset's internal ids. The second pass reuses a fingerprint-matched asset and
352 writes no duplicate SVG file. Unmatched flat-only subtrees still extract
353 normally. Use `--clean-stale` on both import-workspace passes to remove stale
354 generated files for their respective prefixes. In create-template workspaces,
355 `imported` is the fixed namespace: assets live once under `icons/imported/`, and
356 the working SVGs reference them as `data-icon="imported/<name>"`. Inventory
357 entries retain source refs from each extracted subtree, allowing expansion to
358 reconnect the authoring-manifest mapping. A rerun on an
359 already rewritten namespaced projection inventories those references and does
360 not progressively extract their remaining parent or sibling geometry. An
361 in-place pass over an authoring bundle refreshes `authoring_summary.json`
362 automatically.
363
364 ## `mirror_template_materialize.py`
365
366 Compile one Type A PPTX import workspace into a deterministic structured mirror
367 template after the layered authoring IR has been reviewed and edited:
368
369 ```bash
370 python3 scripts/mirror_template_materialize.py \
371 <import_workspace> <empty_template_workspace>
372 ```
373
374 The command treats `<import_workspace>/authoring-svg/` as the sole editable
375 source. It reads the tool-only layered authoring manifest internally and
376 validates it against immutable lossless SVG
377 hashes, source PPTX hash, complete Master/Layout/Slide graph, inheritance
378 visibility facts, source-ref closure, and extracted-vector inventory before it
379 writes anything. It refuses a non-empty destination and stages the whole result
380 before atomic publication, so a failed preflight cannot leave a partial
381 template.
382
383 Materialization preserves source page order and emits one definition-only
384 `layout_<layout_key>.svg` for every source Layout unused by all source Slides.
385 It mechanically expands fixed Master/Layout group wrappers into direct atoms,
386 rehydrates only unchanged converter-supported Slide-local/slot refs, keeps the
387 current SVG fallback for edited refs, preserves explicit text hard breaks, and
388 removes every IR-only source ref. Imported axis-flipped groups retain their
389 geometry reflection while descendant SVG text receives a matching
390 counter-reflection, preserving PowerPoint's upright glyph appearance in browser
391 previews. Supported opaque `p:txBody`,
392 relationship-free `p:style`, and `a:custGeom` payloads are deduplicated into
393 `templates/native_payloads.json.gz`. Repeated native restoration attributes
394 are stored there as short `data-pptx-native-ref` records; page and
395 imported-vector SVGs retain only those record ids and content-hash payload
396 references. The native record referenced by an imported text placeholder
397 carrier owns its authoritative source frame, so the Slide-local frame can
398 differ from reusable Layout bounds without restoring long exact coordinates
399 inline. Structural Master/Layout, placeholder, layer, and editable-object
400 fields remain inline. Source `p:sldLayout@showMasterSp` and
401 `p:sld@showMasterSp` facts become canonical root
402 `data-pptx-show-master-shapes` and
403 `data-pptx-show-inherited-shapes` booleans.
404
405 Checker, template-structure validation, and export hydrate both store layers in
406 memory; legacy inline payload and v1 payload-only stores remain readable.
407
408 The published `ppt-master.template-execution-manifest.v1` roster points to one
409 compact `ppt-master.template-text-slots.v2-min` sidecar per prototype. Each text
410 slot contains only `selector`, `role`, `current_text`, `text_segments`, and
411 `tspan_count`; a top-level tool hash covers its selectors and immutable
412 text/tspan topology and attributes. These records are deterministic tool
413 diagnostics, not page-authoring inputs. Page-context emits only the complete
414 prototype's path and SHA for that reference, so the model reads the SVG once
415 per execution context and reuses it until the SHA changes. The model chooses
416 semantics and edits only existing visible text values, while checker and
417 structured export validate output attributes, text/tspan topology, and
418 referenced-resource hashes against
419 the prototype.
420
421 The output routes reusable vectors once to `icons/imported/`, bitmaps to
422 `images/`, and other referenced files to `templates/assets/`. The JSON report
423 reports payload occurrence, native-record, unique-byte, and compressed-store
424 counts and is written to stdout only. The command intentionally does not create
425 `templates/design_spec.md`; Template_Designer writes the package-specific rules
426 and page roster after materialization. This compiler is for Type A mirror materialization,
427 not `standard` / `fidelity`, loose Type B SVGs, ordinary generation, finalize,
428 or export.
429
430 ## `extract_svg_pictures.py`
431
432 Normalize one deliberately selected complex SVG object into one PowerPoint
433 picture. The command accepts exact `<g id>` values only, writes each group as a
434 tight standalone SVG asset, embeds its local image/CSS dependencies, and
435 replaces the source group at the same parent index with one `<image>`. Native
436 export therefore emits one `p:pic` backed by SVG media.
437
438 ```bash
439 python3 scripts/extract_svg_pictures.py \
440 "<workspace>/authoring-svg/<layered_svg_file>.svg" \
441 --select "<group_id>" \
442 --resource-root "<workspace>" \
443 --images-dir "<workspace>/picture-assets" \
444 --inplace
445 ```
446
447 Imported PowerPoint groups normally provide `data-pptx-frame`, which is used
448 as the picture bounds. For a large standalone SVG without frame metadata, the
449 tool measures the selected group with Playwright; use repeated
450 `--bounds ID=x,y,width,height` values when browser measurement is unavailable
451 or when effect overflow needs an explicit frame. `--padding` expands the
452 chosen bounds. The generated `*_picture_asset_inventory.json` records the
453 bounds source, asset hash, copied definition ids, and embedded local resources.
454 Nested selections are accepted only through metadata-only `<g>` ancestors.
455 When an ancestor carries a transform, style, clip, opacity, or other visual
456 attribute, select that outer group instead; this prevents applying the ancestor
457 effect once inside the SVG asset and again to the replacement `<image>`.
458 Scripts, `foreignObject`, SVG animation, remote resources, and external SVG
459 fragment references fail closed; local image/CSS resources must stay inside
460 the declared `--resource-root` and are embedded into the asset.
461 An in-place rewrite inside an authoring bundle refreshes
462 `authoring_summary.json` automatically.
463
464 This operation belongs only to an explicit `create-template` normalization
465 decision in `standard` or `fidelity` mode. It does not choose groups, detect
466 repetition, infer a Master/Layout, or run during ordinary import, free
467 generation, mirror materialization, finalize, or export. Placeholder, native
468 single-shape, table/chart, icon-placeholder, and authored-preset groups are
469 rejected because they already own a different semantic route.
470
471 Do not confuse this tool with `extract_svg_assets.py`:
472
473 - `extract_svg_assets.py` is a model-readability optimization. It replaces
474 heuristic vector runs with `<use data-icon>`, then re-inlines them before
475 export so the PPTX still contains native shapes.
476 - `extract_svg_pictures.py` is an explicit representation change. It replaces
477 only named groups with `<image>`, so each result intentionally remains one
478 editable PowerPoint picture rather than individually editable paths.
479
480 ## Recommended Pipeline
481
482 Run these steps one at a time. Wait for each command to exit successfully before
483 starting the next command.
484
485 When the effective Speaker Notes outcome in `design_spec.md §I` is enabled, run:
486
487 ```bash
488 python3 scripts/total_md_split.py <project_path>
489 ```
490
491 After `total_md_split.py` exits successfully, run:
492
493 ```bash
494 python3 scripts/finalize_svg.py <project_path>
495 ```
496
497 After `finalize_svg.py` exits successfully, run:
498
499 ```bash
500 python3 scripts/svg_to_pptx.py <project_path>
501 ```
502
503 When Speaker Notes is disabled, skip `total_md_split.py` and use
504 `python3 scripts/svg_to_pptx.py <project_path> --no-notes` for the final
505 command. This prevents stale files under `notes/` from being embedded.
506
507 Do not start another post-processing command while the current command is still
508 running. The canonical gates and success criteria are owned by
509 [`generate-pptx.md`](../../workflows/generate-pptx.md) Step 7.
510
511 ## `finalize_svg.py`
512
513 Unified post-processing entry point. This is the preferred way to run SVG cleanup.
514
515 It aggregates:
516 - `embed_icons.py`
517 - static same-document `<use>` expansion from `svg_to_pptx/use_expander.py`
518 - `align_embed_images.py` (`crop-images` / `fix-aspect` / `embed-images` aliases route here)
519 - `flatten_tspan.py`
520
521 `svg_final/` remains a required Step 7.2 artifact even though the native exporter reads `svg_output/`. It is the self-contained visual reference and may be manually inserted as an SVG picture.
522
523 ## `svg_to_pptx.py`
524
525 Convert project SVGs into PPTX.
526
527 ```bash
528 python3 scripts/svg_to_pptx.py <project_path>
529 # Explicit compact image export:
530 python3 scripts/svg_to_pptx.py <project_path> --image-sizing display --image-scale 2 --image-quality 85
531 # Force original image bytes:
532 python3 scripts/svg_to_pptx.py <project_path> --no-image-optimize
533 python3 scripts/svg_to_pptx.py <project_path> --native-charts-and-tables
534 python3 scripts/svg_to_pptx.py <project_path> --pptx-structure structured # deck/layout template override
535 python3 scripts/svg_to_pptx.py <project_path> --pptx-structure flat # free-design/brand-only override
536 # Template-import visual round-trip diagnostic only:
537 python3 scripts/svg_to_pptx.py <template_import_output> -s svg-flat
538 # Post-processed-source comparison diagnostic only (never a release export):
539 python3 scripts/svg_to_pptx.py <project_path> -s final
540 python3 scripts/svg_to_pptx.py <project_path> --no-notes
541 python3 scripts/svg_to_pptx.py <project_path> -t none
542 python3 scripts/svg_to_pptx.py <project_path> --auto-advance 3
543 python3 scripts/svg_to_pptx.py <project_path> --animation mixed --animation-duration 0.8
544 python3 scripts/svg_to_pptx.py <project_path> --reflow-text # opt-in PowerPoint reflow
545 python3 scripts/svg_to_pptx.py <project_path> --no-merge # one text frame per visual line
546 python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio
547 python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio --animation-config animations.json
548 python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio --no-animations
549 ```
550
551 Native image export defaults to `--image-sizing cap`: it preserves source bytes
552 when no resize or EXIF geometry normalization is required, and re-encodes only
553 images that require one of those transformations. The `display` command above
554 is an explicit compact export; `--no-image-optimize` disables all native image
555 optimization and forces original bytes.
556
557 The normal command reads `pptx_structure.mode` from `spec_lock.md`. For legacy
558 projects whose lock exists but predates that field, export emits one compatibility
559 warning and uses `flat`; no SVG regeneration is required. A missing `spec_lock.md`,
560 an explicit legacy/unknown mode, or a requested `structured` export without an
561 explicit current structured contract remains blocking.
562
563 Explicit direct generation may use the
564 [`quick-generate`](../../workflows/profiles/quick-generate.md) profile after the
565 current agent has converted/read sources, researched identified factual gaps,
566 and prepared the required images, icons, formulas, and resource manifests as
567 needed. That profile skips Strategist, Confirm UI, `design_spec.md`, and
568 `spec_lock.md`; it does not skip the resources required by the authored pages.
569 After the complete SVG roster exists, run its lockless final checker, then
570 export:
571
572 ```bash
573 python3 scripts/svg_quality_checker.py <project_path> \
574 --quick-generate --stage final --json
575 python3 scripts/svg_to_pptx.py <project_path> --quick-generate
576 ```
577
578 This direct-export flag takes `svg_output/` as its authored page source, resolves
579 valid project-local resources referenced by those pages, infers one consistent
580 canvas, uses flat converter-default package scaffolding, and does not read or
581 require `spec_lock.md`. Notes, motion, narration, native objects, conversion
582 trace, and other ordinary exporter capabilities remain available; notes,
583 custom object animation, and narration start off in Quick and may be enabled
584 when needed. The exporter refuses a missing, blocking, non-final, or stale
585 Quick final report before PPTX creation. Default-path output retains the normal
586 postflight report and `backup/` snapshot; explicit `-o` retains the ordinary
587 no-backup behavior. Existing source, analysis, image/icon/formula, and
588 resource-manifest artifacts remain untouched.
589
590 For generated-project narration, follow the
591 [`generate-audio`](../../workflows/stages/generate-audio.md) stage. It owns voice
592 selection, audio generation, and the narrated re-export workflow.
593
594 Behavior:
595 - Default output (either Generate profile, no `-o`):
596 - `exports/<project_name>_<timestamp>.pptx` — native editable pptx (canonical output)
597 - `validation/<project_name>_<timestamp>.report.json` — package postflight, quality-gate linkage, unresolved resource audit, and published part counts
598 - `backup/<timestamp>/svg_output/` — copy of authored SVG source for re-export without re-running the LLM
599 - `exports/` contains only final PPTX deliverables; machine-readable quality and postflight reports belong in `validation/`.
600 - The default Generate flow always runs `finalize_svg.py` before export. This directory is the self-contained SVG visual preview; it is not packaged as a second PPTX. Quick-generate deliberately skips it.
601 - In both Generate profiles, explicit `-o/--output` changes the native PPTX destination and skips `backup/`; the postflight report still uses the output stem under the project `validation/` directory.
602 - Postflight reruns ZIP integrity and published Slide count. Internal relationships,
603 structured-package validation, transitions, and animations are enforced before the
604 builder publishes the PPTX and are reported as `enforced-at-build`, not as repeated
605 postflight checks.
606 - `font_portability` warns when a complete font stack has no concrete family or when
607 the converter resolves its Latin / East Asian role to a typeface that normally
608 requires a custom installation. A recommended stack such as
609 `"Microsoft YaHei", Arial, sans-serif` does not warn merely because it ends with a
610 generic fallback.
611 - Multiline text export modes:
612 - Default: one editable frame retains authored breaks and disables PowerPoint wrapping. An ordinary generated frame uses PowerPoint's native resize-shape-to-fit-text behavior, so deleting a retained break expands the frame instead of leaving text outside it; imported exact frames and structured multiline placeholder carriers retain fixed-size behavior.
613 - `--reflow-text`: eligible same-size lines become flowing prose that PowerPoint may rewrap; a font-size change, list marker, or accepted larger gap remains a paragraph boundary. Legacy `--merge-paragraphs` aliases this mode.
614 - `--no-merge`: each dy-stacked line becomes an independent frame with its own placement.
615 - Detection is conservative: mixed-layout `<text>` falls back to per-line frames. Use `--reflow-text` only for resizable body copy and `--no-merge` only for independent line objects or absolute line positions.
616 - Native release export reads `svg_output/`. `-s final` is an explicit diagnostic override for comparing conversion behavior against post-processed SVGs; it does not change artifact ownership or create a supported release path.
617 - `svg_final/` may be opened directly or inserted into PowerPoint as an SVG picture. PowerPoint's manual Convert-to-Shape operation is outside the compatibility contract.
618 - On every SVG-authoring route, each file in `svg_output/` is the complete visible
619 page-design source. Templates and locks may guide authoring, but finalize/export
620 never use them to overlay visible content missing from the SVG. Notes, animation,
621 narration, transitions, and direct native-PPTX workflows keep their separate
622 inputs and package-level processing.
623 - For PPTX template-import workspaces, use `-s svg-flat` when you need a visual round-trip check. The layered `svg/` tree is the machine-readable template source and intentionally does not inline inherited master / layout decoration into each slide.
624 - Native mode is strict about unsupported visual SVG elements: if a visual element cannot be represented or safely preserved, export fails with the SVG file, element tag, and position instead of silently dropping content.
625 - Omitting `--pptx-structure` reads `spec_lock.md`. Free-design, brand-only, and `template_reuse_scope: style` releases declare `mode: flat`, omit Master/Layout mappings and SVG structure metadata, and materialize one clean project-owned Master plus one Blank Layout from the current lock. Deck/layout templates use `mode: structured` only for `template_reuse_scope: mirror|layout`, with complete unique `pptx_masters` / `pptx_layouts` rosters and one `page_pptx_layouts` assignment per page. A template-backed Layout definition may remain unused by pages and still register in the final package.
626 - On structured template routes, every page root repeats Master/Layout keys and picker names. Master/Layout fixed visuals are direct semantic atoms. Ordinary layer `<g>` elements are invalid; one validated compact authored-preset `<g>` emitted by `preset_shape_svg.py` is the sole group exception because it compiles to one native shape.
627 - Every visible direct root `<g>` requires root-coordinate `data-pptx-bounds`; nested bounds are ignored. Frame/native metadata never replaces it; placeholder bounds also define the slot frame. Checker compares root bounds with `viewBox`, descendant text with its module using DrawingML wrapping headroom, and every estimable visible text carrier directly with the root `viewBox` before that headroom. Images, shapes, paths, `<use>`, effects, and object frames are excluded from module containment. Per side, ≤`1px` is ignored; module overflow ≤`5%` warns and >`5%` fails, while larger page text overflow always fails. Bounds never clip/reflow; unestimable visible text warns. A wholly off-canvas direct-root Morph endpoint may opt out of page containment with `data-pptx-morph-staging="true"`; it still needs valid module bounds, retained Morph uses an explicit pair, and partial overflow remains blocking.
628 - Missing root bounds fails on final pages/templates and under `--template-mode`; references warn until adapted.
629 - On structured template routes, each normal slot is a direct root `<g id>` with semantic type, positive design-zone bounds, and exactly one compatible carrier. Composite `object` slots use explicit proxy binding; zero-slot Layouts are valid. Flat pages keep all SVG objects Slide-local.
630 - Flat export maps locked typography/colors into a clean project-owned theme/Master, removes stock content placeholders and unused built-in Layouts, retains only the standard date/footer/slide-number capability hooks, and keeps one Blank Layout without promoting Slide content. Structured export additionally creates one reusable Layout per declared key and reopens the package to verify the full Presentation → Master → Layout → Slide graph, fixed-object order, placeholder identities/bounds, carrier bindings, hidden proxies, and zero-slot Layouts.
631 - Template `page_layouts` remains input provenance. Strict preserves the prototype contract; adaptive retains its Master and may use a new Layout identity only when Strategist declared it in the plan and lock. Construction cannot allocate or mutate Layout identity downstream.
632 - Legacy structured/template contracts using `baseline`, `template`, `preserve`, `layout_strategy`, `data-pptx-layout-kind`, `distilled`/`utility`, direct atomic placeholders, or incomplete Master identity are rejected with a pointer to [`create-template`](../../workflows/create-template.md). Create a new workspace and generate new structured SVG pages; do not upgrade the existing project in place. Explicit flat free-design/brand-only projects intentionally omit Master identity.
633 - Native output uses content-hash media filenames, so identical images are reused and different images cannot overwrite each other by sharing a basename.
634 - `[Content_Types].xml` is generated from the actual media extensions written into the PPTX. Unknown media extensions fail unless Python's `mimetypes` can identify them.
635 - Native export writes to a temporary file first and publishes the requested PPTX only after conversion succeeds. A failed conversion does not replace the main output file.
636 - `--conversion-trace` without a path writes `validation/<output_stem>.trace.json`. `--conversion-trace <path>` respects the explicit destination; relative paths are resolved from the project root, so `exports/<name>.trace.json` remains available when intentionally requested.
637 - Formal default and `--quick-generate` release export compute the exact SVG source fingerprint and refuse a missing, unreadable, unsupported, non-final, blocking, stale, or unverifiable final quality report before PPTX creation. A project without `validation/svg_quality_report.json` exits nonzero with the `not-provided` gate status; run the final checker against its current `svg_output/` first. An explicit non-`output` `--source` remains a diagnostic override and bypasses this release gate; postflight still records any verifiable report linkage.
638 - After publication, native export writes `validation/<output_stem>.report.json`. The report distinguishes authored Slides from internal Layout definitions, reruns ZIP integrity and published Slide-count checks, records slide/layout/master/notes part counts, labels relationship/structured/transition/animation validation as enforced at build time, links the final SVG quality report only when its SHA-256 source fingerprint matches the exact export inputs, and surfaces stale/unverified gates, unresolved template tokens, generic-only font stacks, and external image references. A matching final quality report with introduced warnings yields `passed-with-warnings` and a `quality_introduced_warnings=<N>` receipt instead of a clean `passed` claim.
639 - By default, a successful command also prints a compact receipt instead of requiring a report read: `[POSTFLIGHT] status=<...> quality_gate=<...> slides=<N> warning_categories=<N>`, followed by one compact line per warning category and the `[PPTX]` / `[REPORT]` paths. Resource-warning lines carry counts; a non-passing quality gate carries its status. Routine agents use this receipt and do not load either complete validation JSON into model context. Full reports remain cold audit artifacts; failure investigation and explicit audits extract only the required fields. `--quiet` keeps suppressing successful-run output.
640 - Before publishing structured template output, export reopens the temporary PPTX and validates the Slide → Layout → Master graph and registrations, Layout identity, placeholder identity, reusable bounds, and prompt/level-one sizes. A mismatch aborts publication. Flat release instead validates its single referenced Master/Layout shell and exact date/footer/slide-number hook roster before packaging.
641 - Authored SVG clip-path restrictions remain. Crop wrappers use an
642 overflow-hidden viewport; preview-safe shape clips target the inner image in
643 viewBox coordinates, while legacy imported wrapper clips remain compatible.
644 Both map to native picture crop/geometry when possible.
645 - The default Generate flow embeds speaker notes automatically unless `--no-notes` is used; Quick Generate defaults them off and enables them with `--with-notes`
646 - Recorded narration is opt-in:
647 - `notes_to_audio.py` uses `edge-tts` by default, or a configured cloud TTS provider (`elevenlabs`, `minimax`, `qwen`, `cosyvoice`), and generates one audio file per slide into `audio/`
648 - 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
649 - `--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
650 - `--recorded-narration audio` keeps speaker notes, embeds each matching audio file, and writes slide auto-advance timings from audio duration
651 - When either animation sidecar exists, narrated export defaults to `<project>/narration_animations.json`; a canonical `animations.json` without that derived file remains a blocking synchronization error
652 - 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
653 - Non-narrated export keeps the existing optional `<project>/animations.json` default
654 - 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
655 - `--narration-audio-dir audio` is the lower-level embedding path: it embeds whatever files match and allows partial audio coverage
656 - Either narration flag names the default-flow export `<project_name>_<timestamp>_narrated.pptx`, telling it apart from silent exports in the same directory
657 - This is intended for direct PowerPoint video export with "Use recorded timings and narrations"
658 - Long-audio import and automatic long-audio splitting are not supported; keep narration assets page-level
659 - Voice choices can be listed with `python3 scripts/notes_to_audio.py --list-common-voices`, `python3 scripts/notes_to_audio.py --list-voices --locale zh-CN`, or provider-specific `--provider <name> --list-voices`
660 - Page transitions are controlled by `-t/--transition`; per-element object animations are controlled by `-a/--animation`
661 - Per-element animation applies to ordinary top-level SVG `<g id="...">` groups; each group is a PowerPoint shape-target anchor, not necessarily one Animation Pane row. Use one group per logical Slide-local content unit rather than targeting a group count. Master/Layout atoms and slot groups are structural and excluded; exact id tokens remain a fallback only when explicit structural roles are absent
662 - An explicit `animations.json` group entry may override the marker-free legacy chrome-name heuristic. It cannot override `data-pptx-layer` or an explicit static role/placeholder marker
663 - Start mode is set globally by `--animation-trigger`, mirroring PowerPoint's Start dropdown: `after-previous` (default, cascade with `--animation-stagger` spacing on slide entry), `on-click` (presenter-paced), or `with-previous` (all together on slide entry). A sidecar row may override it with `trigger`; the slide value is only the inherited Start mode
664 - `on-click` is for live presentations only; recorded narration rejects every row that resolves to it, including a row with `trigger_shape`, because the tool does not generate object-level click timings
665 - Flat SVG roots without top-level groups fall back to at most 8 visible primitives; beyond that, animation is skipped on the slide
666 - Per-element animation defaults to `none`. `auto` is opt-in (`-a auto`) and maps
667 generic entrance effects from the group's SVG id: information-dense elements
668 get a stable entrance (chart→wipe, card-/step-/pillar-→fly,
669 title/takeaway→fade); image-like and unmatched ids rotate through bounded
670 entrance pools.
671 - `mixed` (legacy) deterministically rotates through the canonical entrance pool; `random` selects from the same entrance pool with a stable seed from the effective deck input. `auto`, `mixed`, and `random` never choose emphasis, motion-path, or exit effects; select an explicit canonical `entrance_*`, `emphasis_*`, `path_*`, or `exit_*` key for those authored duties. `--conversion-trace` records each resolved effect when enabled
672 - `--animation-duration` controls the inherited per-row schedule length (default
673 `0.4`); scalable native effects preserve internal timing ratios, while
674 instantaneous presets keep their authored duration. `--animation-stagger`
675 supplies the default gap between successive non-trigger-shape rows in
676 `after-previous` mode (default `0.5`)
677 - Optional object-level overrides live in `<project>/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
678 - 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`
679 - 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
680 - 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
681 - The animation writer does not emit paragraph/text-range builds (`p:bldP`), custom freeform motion paths, native Chart/SmartArt build sequences, or media playback commands for grouped SVG content. Direct-PPTX routes preserve source object animation and perform structural package validation only; they do not author effects
682 - The full registry, OOXML rules, and compatibility boundary are documented in [`pptx-animations.md`](./pptx-animations.md)
683
684 Dependency:
685
686 ```bash
687 pip install python-pptx
688 ```
689
690 ## `total_md_split.py`
691
692 Split `total.md` into per-slide note files.
693
694 ```bash
695 python3 scripts/total_md_split.py <project_path>
696 python3 scripts/total_md_split.py <project_path> -o <output_directory>
697 python3 scripts/total_md_split.py <project_path> -q
698 ```
699
700 Requirements:
701 - Each section begins with `# `
702 - Heading text matches the SVG filename
703 - Sections are separated by `---`
704
705 ## `svg_quality_checker.py`
706
707 Validate SVG technical compliance.
708
709 ```bash
710 python3 scripts/svg_quality_checker.py examples/project/svg_output/01_cover.svg
711 python3 scripts/svg_quality_checker.py examples/project/svg_output
712 python3 scripts/svg_quality_checker.py examples/project
713 python3 scripts/svg_quality_checker.py examples/project --stage first-page
714 python3 scripts/svg_quality_checker.py examples/project --stage final --json
715 python3 scripts/svg_quality_checker.py examples/project --format ppt169
716 python3 scripts/svg_quality_checker.py --all examples
717 python3 scripts/svg_quality_checker.py examples/project --export
718 python3 scripts/svg_quality_checker.py path/to/template/templates --template-mode
719 ```
720
721 Checks include:
722 - `viewBox`
723 - banned elements
724 - paint compatibility: unsupported values error; supported non-default spellings such as `rgba()` receive non-blocking recommendations for `#RRGGBB` plus explicit alpha
725 - line-break structure
726 - explicit Master/Layout/slot structure for reusable templates
727 - duplicate empty Layout contracts under different keys
728
729 Warnings are advisory: they require no modification or acknowledgement and do
730 not affect the command's zero exit status. Only errors block the quality gate.
731
732 `--stage first-page` resolves only the first authored SVG and permits an incomplete
733 future page roster. `--stage final` checks the complete project. With `--json`,
734 the final stage writes `validation/svg_quality_report.json`, while the first-page
735 stage writes `validation/svg_quality_first_page_report.json` so it cannot overwrite
736 the release gate (or use `--json-output`). The report separates
737 release failures (`blocking`), changed/new advisories (`introduced`),
738 prototype-identical diagnostics (`inherited`), and source-conversion losses
739 (`source-import`). It also fingerprints every checked SVG so postflight cannot
740 mistake a stale report for the current export gate. On a successful run, use the
741 checker exit status and terminal summary; do not load the complete JSON unless a
742 failure investigation or explicit audit requires targeted fields.
743
744 Template mode accepts the same compact canonical preset groups as generated
745 pages: one atomic `<g data-pptx-authoring="preset">` with direct visible paths.
746 It validates those paths dynamically against the locked registry and does not
747 require an import-style carrier, preview wrapper, fingerprint, or a separate
748 source-payload opt-in marker. Exact syntax remains owned by the linked
749 standards rather than this pipeline overview.
750
751 ## `svg_position_calculator.py`
752
753 Analyze and review supported chart coordinates after SVG generation.
754
755 Use this after `svg_quality_checker.py` passes, and only for chart types supported by this script: `bar`, `pie` / `donut`, `radar`, `line` / `area` / `scatter`, and `grid`. Area charts do not have a separate calculator mode: use `calc line` for the upper boundary points, then close the filled region to the plot area's bottom baseline (`y_max`) in the SVG.
756
757 ### Calculate expected coordinates
758
759 ```bash
760 python3 scripts/svg_position_calculator.py calc bar --data "A:185,B:142" --area "130,155,1200,480" --bar-width 120
761 python3 scripts/svg_position_calculator.py calc line --data "0:50,10:80,20:120" --area "120,120,1200,600" --y-range "0,150"
762 python3 scripts/svg_position_calculator.py calc pie --data "A:35,B:25,C:20" --center "420,400" --radius 200
763 python3 scripts/svg_position_calculator.py calc grid --rows 2 --cols 3 --area "50,150,1230,670"
764 ```
765
766 For an area chart, use the line output as the top boundary:
767
768 ```svg
769 M first_x,first_y ... L last_x,last_y L last_x,y_max L first_x,y_max Z
770 ```
771
772 Manually compare the calculator output with the coordinates already present in the generated SVG. If coordinates differ, update the SVG from the `calc` output, rerun `svg_quality_checker.py`, then repeat the coordinate review. The tool intentionally does not rewrite SVG files automatically.
773
774 ### Analyze (inspect existing SVG)
775
776 ```bash
777 python3 scripts/svg_position_calculator.py analyze <svg_file>
778 ```
779
780 Use this after SVG generation to inspect existing SVG geometry when manual comparison needs more context.
781
782 ## Advanced Standalone Tools
783
784 ### `flatten_tspan.py`
785
786 ```bash
787 python3 scripts/svg_finalize/flatten_tspan.py examples/<project>/svg_output
788 python3 scripts/svg_finalize/flatten_tspan.py path/to/input.svg path/to/output.svg
789 ```
790
791 ### `align_embed_images.py`
792
793 ```bash
794 python3 scripts/svg_finalize/align_embed_images.py path/to/slide.svg
795 python3 scripts/svg_finalize/align_embed_images.py --dry-run path/to/slide.svg
796 ```
797
798 Use for rare single-file diagnostics when image `slice` / `meet` alignment and
799 Base64 embedding must be inspected outside `finalize_svg.py`. In normal project
800 runs, use `python3 scripts/finalize_svg.py <project_path>`; the old
801 `crop-images`, `fix-aspect`, and `embed-images` names remain accepted only as
802 `finalize_svg.py --only` aliases for the merged `align-images` step.
803
804 ### `embed_icons.py`
805
806 ```bash
807 python3 scripts/svg_finalize/embed_icons.py output.svg
808 python3 scripts/svg_finalize/embed_icons.py svg_output/*.svg
809 python3 scripts/svg_finalize/embed_icons.py --dry-run svg_output/*.svg
810 ```
811
812 Replaces `<use data-icon="chunk-filled/name" .../>`, `<use data-icon="tabler-filled/name" .../>` and `<use data-icon="tabler-outline/name" .../>` placeholders with actual SVG path elements. Use for manual icon embedding checks outside `finalize_svg.py`.
813
814 ## SVG Compatibility Contract
815
816 The always-on SVG authoring contract lives in
817 [`shared-standards-core.md`](../../references/shared-standards-core.md), with
818 advanced effects, native data objects, and structured PPTX metadata owned by
819 their conditionally loaded modules. This tool guide does not repeat accepted
820 syntax, rejected constructs, or conditional limits.
821
822 `svg_quality_checker.py` validates source SVG before finalization.
823 `finalize_svg.py` and native export apply the preprocessing required by that
824 contract, while native conversion fails on unsupported visual elements rather
825 than silently dropping them.
826
826 lines MARKDOWN