pptx-cli 1.2.5__tar.gz → 1.2.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx/SKILL.md +4 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/SKILL.md +7 -1
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/PKG-INFO +16 -1
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/PRD.md +11 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/README.md +15 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/__init__.py +1 -1
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/cli.py +25 -4
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/compose.py +47 -5
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/guide.py +8 -2
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/composition.py +40 -2
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/models/manifest.py +1 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/tests/test_cli.py +176 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.editorconfig +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/copilot-instructions.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/instructions/backend.instructions.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/instructions/testing.instructions.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx/references/deck-spec.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/references/excal-diagrams.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/references/mckinsey-style.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/references/pptx-workflow.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/workflows/ci.yml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/workflows/publish.yml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.gitignore +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/.python-version +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/AGENTS.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/ARCHITECTURE.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/CHANGELOG.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/CLI-MANIFEST.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/CONTRIBUTING.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/DECISIONS/ADR-0001-initial-architecture.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/LICENSE +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/PREVIEW-FUTURE.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/PROJECT.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/SCAFFOLD.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/SECURITY.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/TESTING.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/docs/DOMAIN.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/docs/GLOSSARY.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/docs/ROADMAP.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/docs/SCAFFOLDING-NOTES.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/guardrails/.gitignore +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/guardrails/guardrails.jsonl +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/guardrails/links.jsonl +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/guardrails/references.jsonl +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/guardrails/taxonomy.json +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/guardrails-explorer.html +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/pyproject.toml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/pyrightconfig.json +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/scripts/bump_version.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/__main__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/__init__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/init.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/inspect.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/manifest_ops.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/validate.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/commands/wrapper.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/__init__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/ids.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/io.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/manifest_store.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/markdown.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/runtime.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/template.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/validation.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/core/versioning.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/models/__init__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/src/pptx_cli/models/envelope.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/tests/conftest.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/tests/test_versioning.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.2.6}/uv.lock +0 -0
|
@@ -154,6 +154,10 @@ pptx validate --manifest ./.pptx --deck ./out/deck.pptx --strict --format json
|
|
|
154
154
|
- `chart` — chart data
|
|
155
155
|
- `markdown-to-text` — markdown converted to formatted text
|
|
156
156
|
|
|
157
|
+
Speaker notes are supported separately from placeholder content types. Use a
|
|
158
|
+
slide-level `notes` field in deck specs, or `pptx slide create --notes` / `--notes-file`
|
|
159
|
+
for single-slide generation.
|
|
160
|
+
|
|
157
161
|
## Error handling
|
|
158
162
|
|
|
159
163
|
All commands return a structured JSON envelope with `ok`, `errors`, and `warnings` fields. Key exit codes:
|
|
@@ -184,6 +184,11 @@ slides:
|
|
|
184
184
|
- Bullet point one
|
|
185
185
|
- Bullet point two
|
|
186
186
|
source: "Source: Data attribution"
|
|
187
|
+
notes: |
|
|
188
|
+
# Speaker notes
|
|
189
|
+
|
|
190
|
+
- Open with the takeaway before reading the slide
|
|
191
|
+
- Keep timing to under 90 seconds
|
|
187
192
|
|
|
188
193
|
- layout: picture-layout-id
|
|
189
194
|
content:
|
|
@@ -202,6 +207,7 @@ slides:
|
|
|
202
207
|
- `@notes.md` via `--set key=@notes.md` or `{ kind: "markdown-text", value:
|
|
203
208
|
"..." }` → markdown parsed with headings, lists, inline emphasis, and light
|
|
204
209
|
presentation-aware spacing
|
|
210
|
+
- Slide-level speaker notes → `notes: |` in the deck spec, or `pptx slide create --notes/--notes-file` for single-slide generation
|
|
205
211
|
- `{ kind: "image", path: "path/to/file.png" }` → image insertion
|
|
206
212
|
- `{ kind: "table", columns: [...], rows: [[...], ...] }` → table
|
|
207
213
|
- `{ kind: "chart", chart_type: "column_clustered", categories: [...], series: [{name: "...", values: [...]}] }` → chart
|
|
@@ -399,7 +405,7 @@ pptx theme show --manifest ./manifest-dir --format json
|
|
|
399
405
|
pptx assets list --manifest ./manifest-dir --format json
|
|
400
406
|
|
|
401
407
|
# Single slide (quick test)
|
|
402
|
-
pptx slide create --manifest ./manifest-dir --layout <id> --set title="Hello" --
|
|
408
|
+
pptx slide create --manifest ./manifest-dir --layout <id> --set title="Hello" --notes-file ./speaker-notes.md --out slide.pptx
|
|
403
409
|
|
|
404
410
|
# Full deck
|
|
405
411
|
pptx deck build --manifest ./manifest-dir --spec deck.yaml --out deck.pptx --format json
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pptx-cli
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6
|
|
4
4
|
Summary: Template-bound PowerPoint generation for enterprise decks
|
|
5
5
|
Author: Thomas Rohde
|
|
6
6
|
License: MIT License
|
|
@@ -155,6 +155,7 @@ pptx slide create \
|
|
|
155
155
|
--layout title-only \
|
|
156
156
|
--set title="Enterprise AI Operating Model" \
|
|
157
157
|
--set subtitle="March 2026" \
|
|
158
|
+
--notes-file ./speaker-notes.md \
|
|
158
159
|
--out ./out/operating-model-slide.pptx
|
|
159
160
|
```
|
|
160
161
|
|
|
@@ -209,6 +210,11 @@ slides:
|
|
|
209
210
|
content:
|
|
210
211
|
title: Enterprise AI Operating Model
|
|
211
212
|
subtitle: March 2026
|
|
213
|
+
notes: |
|
|
214
|
+
# Opening talk track
|
|
215
|
+
|
|
216
|
+
- Lead with why preserving the template matters
|
|
217
|
+
- Call out that validation stays available for CI
|
|
212
218
|
- layout: 1-breaker-with-pattern
|
|
213
219
|
content:
|
|
214
220
|
title: Why this change
|
|
@@ -281,6 +287,11 @@ corp-template/
|
|
|
281
287
|
- chart
|
|
282
288
|
- markdown-text
|
|
283
289
|
|
|
290
|
+
Speaker notes are also supported in v1 as optional slide-level metadata via
|
|
291
|
+
`slides[].notes` in deck specs or `pptx slide create --notes/--notes-file`.
|
|
292
|
+
They are not placeholder content types and do not change layout placeholder
|
|
293
|
+
contracts.
|
|
294
|
+
|
|
284
295
|
`markdown-text` is parsed with `markdown-it-py` and currently maps CommonMark blocks into
|
|
285
296
|
PowerPoint paragraphs. Headings become plain paragraphs, bullet lists use native PowerPoint
|
|
286
297
|
bullet levels, ordered lists render as numbered paragraph text, and basic inline emphasis such
|
|
@@ -288,6 +299,10 @@ as bold/italic/code spans is preserved where PowerPoint run formatting can expre
|
|
|
288
299
|
blocks also receive light presentation-aware spacing so headings, paragraphs, and lists do not
|
|
289
300
|
collapse into a dense wall of text.
|
|
290
301
|
|
|
302
|
+
The same markdown-to-text parsing pipeline is used for speaker notes, so headings,
|
|
303
|
+
bullets, ordered lists, and basic inline emphasis remain available in presenter
|
|
304
|
+
notes without introducing a separate formatting model.
|
|
305
|
+
|
|
291
306
|
## Structured content objects
|
|
292
307
|
|
|
293
308
|
`pptx slide create --set picture=@diagram.png` automatically normalizes the file into an
|
|
@@ -248,6 +248,7 @@ pptx slide create \
|
|
|
248
248
|
--set title="Enterprise AI Operating Model" \
|
|
249
249
|
--set left_body=@left.md \
|
|
250
250
|
--set right_body=@right.md \
|
|
251
|
+
--notes-file speaker-notes.md \
|
|
251
252
|
--out slide.pptx
|
|
252
253
|
```
|
|
253
254
|
|
|
@@ -264,6 +265,7 @@ Composition rules:
|
|
|
264
265
|
- only declared placeholders can be filled
|
|
265
266
|
- content types must match placeholder types
|
|
266
267
|
- v1 supported placeholder content types are text, images, tables, charts, and markdown-to-text mappings
|
|
268
|
+
- speaker notes are optional per-slide metadata, not placeholder content types, and may use the same markdown-to-text formatting pipeline
|
|
267
269
|
- v1 tables and charts preserve approved placeholder geometry and accept structured data population, but do not guarantee full preservation of advanced workbook internals or highly custom styling behaviors
|
|
268
270
|
- static brand elements remain untouched unless explicitly marked overridable
|
|
269
271
|
- theme and master dependencies must be preserved
|
|
@@ -378,6 +380,11 @@ The v1 supported placeholder content types shall include:
|
|
|
378
380
|
- chart
|
|
379
381
|
- markdown-to-text
|
|
380
382
|
|
|
383
|
+
The system shall also support optional per-slide speaker notes supplied either
|
|
384
|
+
through the structured deck spec or dedicated direct-command inputs. These notes
|
|
385
|
+
are slide metadata rather than placeholders and reuse the markdown-to-text
|
|
386
|
+
formatting pipeline for rich presenter text.
|
|
387
|
+
|
|
381
388
|
### FR-9: Validate output
|
|
382
389
|
|
|
383
390
|
The system shall validate output decks against the manifest and return machine-friendly errors.
|
|
@@ -669,6 +676,9 @@ slides:
|
|
|
669
676
|
- layout: section-divider
|
|
670
677
|
content:
|
|
671
678
|
title: Why this change
|
|
679
|
+
notes: |
|
|
680
|
+
- Pause before the transition
|
|
681
|
+
- Re-anchor the audience on governance and brand fidelity
|
|
672
682
|
- layout: executive-two-column
|
|
673
683
|
content:
|
|
674
684
|
title: Core idea
|
|
@@ -1132,6 +1142,7 @@ The following product decisions are now fixed for v1:
|
|
|
1132
1142
|
12. Preview metadata uses a single canonical preview path field per layout.
|
|
1133
1143
|
13. The CLI adopts an agent-first machine contract with a structured response envelope, stable error codes, exit-code mapping, and a built-in `guide` command.
|
|
1134
1144
|
14. Mutating commands support `--dry-run` and structured change summaries.
|
|
1145
|
+
15. V1 supports optional per-slide speaker notes using text/markdown formatting, but notes are not part of the placeholder contract and are never required by default.
|
|
1135
1146
|
|
|
1136
1147
|
## 26. Recommendation
|
|
1137
1148
|
|
|
@@ -108,6 +108,7 @@ pptx slide create \
|
|
|
108
108
|
--layout title-only \
|
|
109
109
|
--set title="Enterprise AI Operating Model" \
|
|
110
110
|
--set subtitle="March 2026" \
|
|
111
|
+
--notes-file ./speaker-notes.md \
|
|
111
112
|
--out ./out/operating-model-slide.pptx
|
|
112
113
|
```
|
|
113
114
|
|
|
@@ -162,6 +163,11 @@ slides:
|
|
|
162
163
|
content:
|
|
163
164
|
title: Enterprise AI Operating Model
|
|
164
165
|
subtitle: March 2026
|
|
166
|
+
notes: |
|
|
167
|
+
# Opening talk track
|
|
168
|
+
|
|
169
|
+
- Lead with why preserving the template matters
|
|
170
|
+
- Call out that validation stays available for CI
|
|
165
171
|
- layout: 1-breaker-with-pattern
|
|
166
172
|
content:
|
|
167
173
|
title: Why this change
|
|
@@ -234,6 +240,11 @@ corp-template/
|
|
|
234
240
|
- chart
|
|
235
241
|
- markdown-text
|
|
236
242
|
|
|
243
|
+
Speaker notes are also supported in v1 as optional slide-level metadata via
|
|
244
|
+
`slides[].notes` in deck specs or `pptx slide create --notes/--notes-file`.
|
|
245
|
+
They are not placeholder content types and do not change layout placeholder
|
|
246
|
+
contracts.
|
|
247
|
+
|
|
237
248
|
`markdown-text` is parsed with `markdown-it-py` and currently maps CommonMark blocks into
|
|
238
249
|
PowerPoint paragraphs. Headings become plain paragraphs, bullet lists use native PowerPoint
|
|
239
250
|
bullet levels, ordered lists render as numbered paragraph text, and basic inline emphasis such
|
|
@@ -241,6 +252,10 @@ as bold/italic/code spans is preserved where PowerPoint run formatting can expre
|
|
|
241
252
|
blocks also receive light presentation-aware spacing so headings, paragraphs, and lists do not
|
|
242
253
|
collapse into a dense wall of text.
|
|
243
254
|
|
|
255
|
+
The same markdown-to-text parsing pipeline is used for speaker notes, so headings,
|
|
256
|
+
bullets, ordered lists, and basic inline emphasis remain available in presenter
|
|
257
|
+
notes without introducing a separate formatting model.
|
|
258
|
+
|
|
244
259
|
## Structured content objects
|
|
245
260
|
|
|
246
261
|
`pptx slide create --set picture=@diagram.png` automatically normalizes the file into an
|
|
@@ -30,8 +30,8 @@ from pptx_cli.models.envelope import CliMessage, Envelope, Metrics
|
|
|
30
30
|
app = typer.Typer(
|
|
31
31
|
help=(
|
|
32
32
|
"Template-bound PowerPoint generation for enterprise decks. Supports text, "
|
|
33
|
-
"images, tables, charts,
|
|
34
|
-
"placeholders."
|
|
33
|
+
"images, tables, charts, markdown-text content in template-approved "
|
|
34
|
+
"placeholders, and optional per-slide speaker notes."
|
|
35
35
|
),
|
|
36
36
|
no_args_is_help=True,
|
|
37
37
|
)
|
|
@@ -41,7 +41,10 @@ theme_app = typer.Typer(help="Inspect extracted theme metadata.")
|
|
|
41
41
|
assets_app = typer.Typer(help="Inspect extracted asset references.")
|
|
42
42
|
slide_app = typer.Typer(help="Create slides from approved layouts.")
|
|
43
43
|
deck_app = typer.Typer(
|
|
44
|
-
help=
|
|
44
|
+
help=(
|
|
45
|
+
"Build full decks from structured specs, including markdown-text content "
|
|
46
|
+
"and optional per-slide speaker notes."
|
|
47
|
+
)
|
|
45
48
|
)
|
|
46
49
|
manifest_app = typer.Typer(help="Work with manifest packages and schemas.")
|
|
47
50
|
wrapper_app = typer.Typer(help="Generate thin template-specific wrapper CLIs.")
|
|
@@ -338,6 +341,20 @@ def slide_create_command(
|
|
|
338
341
|
),
|
|
339
342
|
),
|
|
340
343
|
] = None,
|
|
344
|
+
notes: Annotated[
|
|
345
|
+
str | None,
|
|
346
|
+
typer.Option(
|
|
347
|
+
"--notes",
|
|
348
|
+
help="Speaker notes text for the slide. Markdown-looking multiline text is supported.",
|
|
349
|
+
),
|
|
350
|
+
] = None,
|
|
351
|
+
notes_file: Annotated[
|
|
352
|
+
Path | None,
|
|
353
|
+
typer.Option(
|
|
354
|
+
"--notes-file",
|
|
355
|
+
help="Path to a UTF-8 text or markdown file to use as speaker notes.",
|
|
356
|
+
),
|
|
357
|
+
] = None,
|
|
341
358
|
dry_run: DryRunOption = False,
|
|
342
359
|
overwrite: OverwriteOption = False,
|
|
343
360
|
format: FormatOption = None,
|
|
@@ -346,6 +363,7 @@ def slide_create_command(
|
|
|
346
363
|
|
|
347
364
|
Use --set key=@notes.md or a multiline markdown-looking value to populate
|
|
348
365
|
markdown-text placeholders with headings, lists, and inline emphasis.
|
|
366
|
+
Use --notes or --notes-file for slide-level speaker notes.
|
|
349
367
|
"""
|
|
350
368
|
|
|
351
369
|
execute(
|
|
@@ -356,6 +374,8 @@ def slide_create_command(
|
|
|
356
374
|
layout,
|
|
357
375
|
list(set_values or []),
|
|
358
376
|
out,
|
|
377
|
+
notes=notes,
|
|
378
|
+
notes_file=notes_file,
|
|
359
379
|
dry_run=dry_run,
|
|
360
380
|
overwrite=overwrite,
|
|
361
381
|
),
|
|
@@ -383,7 +403,8 @@ def deck_build_command(
|
|
|
383
403
|
"""Build a deck from a structured spec.
|
|
384
404
|
|
|
385
405
|
Deck specs can provide markdown-text content explicitly or rely on multiline
|
|
386
|
-
markdown-looking strings for headings, lists, and inline emphasis.
|
|
406
|
+
markdown-looking strings for headings, lists, and inline emphasis. Each slide
|
|
407
|
+
may also provide an optional `notes` field for speaker notes.
|
|
387
408
|
"""
|
|
388
409
|
|
|
389
410
|
execute(
|
|
@@ -4,6 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from pptx_cli.core.composition import (
|
|
7
|
+
CompositionError,
|
|
7
8
|
build_presentation,
|
|
8
9
|
create_single_slide_spec,
|
|
9
10
|
parse_set_arguments,
|
|
@@ -19,13 +20,17 @@ def slide_create(
|
|
|
19
20
|
set_values: list[str],
|
|
20
21
|
output_path: Path,
|
|
21
22
|
*,
|
|
23
|
+
notes: str | None,
|
|
24
|
+
notes_file: Path | None,
|
|
22
25
|
dry_run: bool,
|
|
23
26
|
overwrite: bool,
|
|
24
27
|
) -> dict[str, Any]:
|
|
25
28
|
manifest = load_effective_manifest(manifest_dir)
|
|
26
29
|
content = parse_set_arguments(set_values)
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
resolved_notes = _resolve_notes_input(notes, notes_file)
|
|
31
|
+
spec = create_single_slide_spec(layout_id, content, notes=resolved_notes)
|
|
32
|
+
notes_changes = _plan_notes_changes(spec)
|
|
33
|
+
planned_changes = [plan_output_change(output_path, overwrite=overwrite), *notes_changes]
|
|
29
34
|
if not dry_run:
|
|
30
35
|
prs = build_presentation(manifest_dir, manifest, spec)
|
|
31
36
|
save_presentation(prs, output_path, overwrite=overwrite)
|
|
@@ -36,7 +41,7 @@ def slide_create(
|
|
|
36
41
|
"out": str(output_path),
|
|
37
42
|
"overwrite": overwrite,
|
|
38
43
|
"changes": planned_changes,
|
|
39
|
-
"summary": {"slides": 1, "artifacts": 1},
|
|
44
|
+
"summary": {"slides": 1, "artifacts": 1, "notes_slides": len(notes_changes)},
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
|
|
@@ -50,7 +55,8 @@ def deck_build(
|
|
|
50
55
|
) -> dict[str, Any]:
|
|
51
56
|
manifest = load_effective_manifest(manifest_dir)
|
|
52
57
|
spec = load_deck_spec(spec_path)
|
|
53
|
-
|
|
58
|
+
notes_changes = _plan_notes_changes(spec)
|
|
59
|
+
planned_changes = [plan_output_change(output_path, overwrite=overwrite), *notes_changes]
|
|
54
60
|
if not dry_run:
|
|
55
61
|
prs = build_presentation(manifest_dir, manifest, spec)
|
|
56
62
|
save_presentation(prs, output_path, overwrite=overwrite)
|
|
@@ -61,6 +67,42 @@ def deck_build(
|
|
|
61
67
|
"out": str(output_path),
|
|
62
68
|
"overwrite": overwrite,
|
|
63
69
|
"changes": planned_changes,
|
|
64
|
-
"summary": {
|
|
70
|
+
"summary": {
|
|
71
|
+
"slides": len(spec.slides),
|
|
72
|
+
"artifacts": 1,
|
|
73
|
+
"notes_slides": len(notes_changes),
|
|
74
|
+
},
|
|
65
75
|
"metadata": spec.metadata,
|
|
66
76
|
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_notes_input(notes: str | None, notes_file: Path | None) -> str | None:
|
|
80
|
+
if notes is not None and notes_file is not None:
|
|
81
|
+
raise CompositionError(
|
|
82
|
+
"ERR_VALIDATION_INPUT",
|
|
83
|
+
"Use either --notes or --notes-file, not both.",
|
|
84
|
+
)
|
|
85
|
+
if notes_file is None:
|
|
86
|
+
return notes
|
|
87
|
+
if not notes_file.exists():
|
|
88
|
+
raise CompositionError(
|
|
89
|
+
"ERR_IO_NOT_FOUND",
|
|
90
|
+
f"Speaker notes file not found: {notes_file}",
|
|
91
|
+
)
|
|
92
|
+
return notes_file.read_text(encoding="utf-8")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _plan_notes_changes(spec: Any) -> list[dict[str, Any]]:
|
|
96
|
+
changes: list[dict[str, Any]] = []
|
|
97
|
+
for slide_index, slide in enumerate(spec.slides, start=1):
|
|
98
|
+
if slide.notes is None:
|
|
99
|
+
continue
|
|
100
|
+
changes.append(
|
|
101
|
+
{
|
|
102
|
+
"target": f"slide[{slide_index}].notes",
|
|
103
|
+
"operation": "create",
|
|
104
|
+
"artifact_type": "speaker-notes",
|
|
105
|
+
"text_length": len(slide.notes),
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
return changes
|
|
@@ -64,12 +64,15 @@ def build_guide_document() -> GuideDocument:
|
|
|
64
64
|
),
|
|
65
65
|
GuideCommand(
|
|
66
66
|
id="slide.create",
|
|
67
|
-
summary="Create a slide from an approved layout",
|
|
67
|
+
summary="Create a slide from an approved layout with optional speaker notes",
|
|
68
68
|
mutates=True,
|
|
69
69
|
input_schema=DeckSpec.model_json_schema(),
|
|
70
70
|
examples=[
|
|
71
71
|
"pptx slide create --manifest ./corp-template --layout title-only "
|
|
72
72
|
"--set title=Hello --out ./out/slide.pptx --dry-run",
|
|
73
|
+
"pptx slide create --manifest ./corp-template --layout title-only "
|
|
74
|
+
"--set title=Hello --notes-file ./speaker-notes.md "
|
|
75
|
+
"--out ./out/slide-with-notes.pptx",
|
|
73
76
|
"pptx slide create --manifest ./corp-template "
|
|
74
77
|
"--layout 3-front-page-title-and-picture "
|
|
75
78
|
"--set title=Workflow --set picture=@out/workflow.png "
|
|
@@ -78,7 +81,7 @@ def build_guide_document() -> GuideDocument:
|
|
|
78
81
|
),
|
|
79
82
|
GuideCommand(
|
|
80
83
|
id="deck.build",
|
|
81
|
-
summary="Build a deck from a structured spec",
|
|
84
|
+
summary="Build a deck from a structured spec with optional per-slide speaker notes",
|
|
82
85
|
mutates=True,
|
|
83
86
|
input_schema=DeckSpec.model_json_schema(),
|
|
84
87
|
examples=[
|
|
@@ -151,6 +154,9 @@ def build_guide_document() -> GuideDocument:
|
|
|
151
154
|
"placeholder_keys": (
|
|
152
155
|
"logical placeholder keys such as title, subtitle, content_1, or picture"
|
|
153
156
|
),
|
|
157
|
+
"slide_notes": (
|
|
158
|
+
"optional per-slide speaker notes via SlideSpec.notes or slide create --notes"
|
|
159
|
+
),
|
|
154
160
|
"manifest_path": "path to a manifest package directory containing manifest.yaml",
|
|
155
161
|
},
|
|
156
162
|
concurrency={
|
|
@@ -118,8 +118,13 @@ def _load_inline_or_file_value(raw_value: str) -> Any:
|
|
|
118
118
|
return raw_value
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def create_single_slide_spec(
|
|
122
|
-
|
|
121
|
+
def create_single_slide_spec(
|
|
122
|
+
layout: str,
|
|
123
|
+
content: dict[str, Any],
|
|
124
|
+
*,
|
|
125
|
+
notes: str | None = None,
|
|
126
|
+
) -> DeckSpec:
|
|
127
|
+
return DeckSpec(slides=[SlideSpec(layout=layout, content=content, notes=notes)])
|
|
123
128
|
|
|
124
129
|
|
|
125
130
|
def build_presentation(manifest_dir: Path, manifest: ManifestDocument, spec: DeckSpec) -> Any:
|
|
@@ -137,6 +142,8 @@ def build_presentation(manifest_dir: Path, manifest: ManifestDocument, spec: Dec
|
|
|
137
142
|
slide_layout = prs.slide_layouts[layout_contract.source_layout_index]
|
|
138
143
|
slide = prs.slides.add_slide(slide_layout)
|
|
139
144
|
_populate_slide(slide, layout_contract, slide_spec.content)
|
|
145
|
+
if slide_spec.notes is not None:
|
|
146
|
+
_apply_slide_notes(slide, slide_spec.notes)
|
|
140
147
|
except CompositionError as exc:
|
|
141
148
|
raise CompositionError(
|
|
142
149
|
exc.code,
|
|
@@ -302,6 +309,37 @@ def _apply_text(shape: Any, text: str, *, markdown: bool) -> None:
|
|
|
302
309
|
paragraphs = parse_markdown_paragraphs(text) if markdown else parse_plain_text_paragraphs(text)
|
|
303
310
|
list_level_offset = _first_markdown_list_level(shape) if markdown else 0
|
|
304
311
|
|
|
312
|
+
_write_text_frame_paragraphs(
|
|
313
|
+
text_frame,
|
|
314
|
+
paragraphs,
|
|
315
|
+
markdown=markdown,
|
|
316
|
+
list_level_offset=list_level_offset,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _apply_slide_notes(slide: Any, text: str) -> None:
|
|
321
|
+
notes_slide = slide.notes_slide
|
|
322
|
+
text_frame = notes_slide.notes_text_frame
|
|
323
|
+
text_frame.clear()
|
|
324
|
+
markdown = looks_like_markdown(text)
|
|
325
|
+
paragraphs = parse_markdown_paragraphs(text) if markdown else parse_plain_text_paragraphs(text)
|
|
326
|
+
|
|
327
|
+
_write_text_frame_paragraphs(
|
|
328
|
+
text_frame,
|
|
329
|
+
paragraphs,
|
|
330
|
+
markdown=markdown,
|
|
331
|
+
list_level_offset=0,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _write_text_frame_paragraphs(
|
|
336
|
+
text_frame: Any,
|
|
337
|
+
paragraphs: list[ParsedParagraph],
|
|
338
|
+
*,
|
|
339
|
+
markdown: bool,
|
|
340
|
+
list_level_offset: int,
|
|
341
|
+
) -> None:
|
|
342
|
+
|
|
305
343
|
previous: ParsedParagraph | None = None
|
|
306
344
|
for index, parsed in enumerate(paragraphs):
|
|
307
345
|
paragraph = text_frame.paragraphs[0] if index == 0 else text_frame.add_paragraph()
|
|
@@ -59,6 +59,11 @@ def test_guide_returns_structured_envelope_with_catalog() -> None:
|
|
|
59
59
|
command_ids = {command["id"] for command in payload["result"]["commands"]}
|
|
60
60
|
assert "layouts.list" in command_ids
|
|
61
61
|
assert "deck.build" in command_ids
|
|
62
|
+
slide_create_command = next(
|
|
63
|
+
command for command in payload["result"]["commands"] if command["id"] == "slide.create"
|
|
64
|
+
)
|
|
65
|
+
slide_spec_properties = slide_create_command["input_schema"]["$defs"]["SlideSpec"]["properties"]
|
|
66
|
+
assert "notes" in slide_spec_properties
|
|
62
67
|
assert payload["result"]["exit_codes"]["validation_error"] == 10
|
|
63
68
|
assert "ERR_IO_NOT_FOUND" in payload["result"]["error_codes"]
|
|
64
69
|
assert payload["result"]["content_objects"]["image"]["example"]["kind"] == "image"
|
|
@@ -257,6 +262,64 @@ def test_slide_create_and_validate_round_trip(
|
|
|
257
262
|
assert title_shape.text_frame.vertical_anchor == template_title_shape.text_frame.vertical_anchor
|
|
258
263
|
|
|
259
264
|
|
|
265
|
+
def test_slide_create_supports_speaker_notes_from_markdown_file(
|
|
266
|
+
template_path: Path,
|
|
267
|
+
manifest_dir: Path,
|
|
268
|
+
tmp_path: Path,
|
|
269
|
+
) -> None:
|
|
270
|
+
out_file = tmp_path / "slide-with-notes.pptx"
|
|
271
|
+
notes_file = tmp_path / "speaker-notes.md"
|
|
272
|
+
notes_file.write_text(
|
|
273
|
+
"# Talking points\n\n- Lead with customer outcomes\n- Close on delivery confidence\n",
|
|
274
|
+
encoding="utf-8",
|
|
275
|
+
)
|
|
276
|
+
_init_manifest(template_path, manifest_dir)
|
|
277
|
+
|
|
278
|
+
payload = _invoke_json(
|
|
279
|
+
[
|
|
280
|
+
"slide",
|
|
281
|
+
"create",
|
|
282
|
+
"--manifest",
|
|
283
|
+
str(manifest_dir),
|
|
284
|
+
"--layout",
|
|
285
|
+
"title-only",
|
|
286
|
+
"--set",
|
|
287
|
+
"title=Quarterly Update",
|
|
288
|
+
"--notes-file",
|
|
289
|
+
str(notes_file),
|
|
290
|
+
"--out",
|
|
291
|
+
str(out_file),
|
|
292
|
+
]
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
assert payload["result"]["summary"]["notes_slides"] == 1
|
|
296
|
+
assert any(change["target"] == "slide[1].notes" for change in payload["result"]["changes"])
|
|
297
|
+
|
|
298
|
+
generated = Presentation(str(out_file))
|
|
299
|
+
notes_frame = generated.slides[0].notes_slide.notes_text_frame
|
|
300
|
+
assert notes_frame is not None
|
|
301
|
+
paragraphs = notes_frame.paragraphs
|
|
302
|
+
assert [paragraph.text for paragraph in paragraphs] == [
|
|
303
|
+
"Talking points",
|
|
304
|
+
"Lead with customer outcomes",
|
|
305
|
+
"Close on delivery confidence",
|
|
306
|
+
]
|
|
307
|
+
assert paragraphs[0].runs[0].font.bold is True
|
|
308
|
+
assert paragraphs[1].level == 0
|
|
309
|
+
assert paragraphs[2].level == 0
|
|
310
|
+
|
|
311
|
+
validation = _invoke_json(
|
|
312
|
+
[
|
|
313
|
+
"validate",
|
|
314
|
+
"--manifest",
|
|
315
|
+
str(manifest_dir),
|
|
316
|
+
"--deck",
|
|
317
|
+
str(out_file),
|
|
318
|
+
]
|
|
319
|
+
)
|
|
320
|
+
assert validation["result"]["ok"] is True
|
|
321
|
+
|
|
322
|
+
|
|
260
323
|
def test_slide_create_picture_placeholder_uses_fit_by_default(
|
|
261
324
|
template_path: Path,
|
|
262
325
|
manifest_dir: Path,
|
|
@@ -409,6 +472,119 @@ def test_deck_build_supports_structured_image_table_and_chart_content(
|
|
|
409
472
|
assert chart_shape.has_chart is True
|
|
410
473
|
|
|
411
474
|
|
|
475
|
+
def test_deck_build_supports_speaker_notes_and_reports_them_in_dry_run(
|
|
476
|
+
template_path: Path,
|
|
477
|
+
manifest_dir: Path,
|
|
478
|
+
tmp_path: Path,
|
|
479
|
+
) -> None:
|
|
480
|
+
deck_spec = tmp_path / "speaker-notes-deck.yaml"
|
|
481
|
+
out_file = tmp_path / "speaker-notes-deck.pptx"
|
|
482
|
+
_init_manifest(template_path, manifest_dir)
|
|
483
|
+
|
|
484
|
+
deck_spec.write_text(
|
|
485
|
+
yaml.safe_dump(
|
|
486
|
+
{
|
|
487
|
+
"slides": [
|
|
488
|
+
{
|
|
489
|
+
"layout": "title-only",
|
|
490
|
+
"content": {"title": "Opening"},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"layout": "1-title-and-content",
|
|
494
|
+
"content": {
|
|
495
|
+
"title": "Operating model",
|
|
496
|
+
"content_1": "Preserve the template contract.",
|
|
497
|
+
},
|
|
498
|
+
"notes": "Lead with the governance angle.\n\n- Mention change control",
|
|
499
|
+
},
|
|
500
|
+
]
|
|
501
|
+
},
|
|
502
|
+
sort_keys=False,
|
|
503
|
+
),
|
|
504
|
+
encoding="utf-8",
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
dry_run_payload = _invoke_json(
|
|
508
|
+
[
|
|
509
|
+
"deck",
|
|
510
|
+
"build",
|
|
511
|
+
"--manifest",
|
|
512
|
+
str(manifest_dir),
|
|
513
|
+
"--spec",
|
|
514
|
+
str(deck_spec),
|
|
515
|
+
"--out",
|
|
516
|
+
str(out_file),
|
|
517
|
+
"--dry-run",
|
|
518
|
+
]
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
assert dry_run_payload["result"]["summary"]["notes_slides"] == 1
|
|
522
|
+
assert any(
|
|
523
|
+
change["target"] == "slide[2].notes" and change["artifact_type"] == "speaker-notes"
|
|
524
|
+
for change in dry_run_payload["result"]["changes"]
|
|
525
|
+
)
|
|
526
|
+
assert out_file.exists() is False
|
|
527
|
+
|
|
528
|
+
build_payload = _invoke_json(
|
|
529
|
+
[
|
|
530
|
+
"deck",
|
|
531
|
+
"build",
|
|
532
|
+
"--manifest",
|
|
533
|
+
str(manifest_dir),
|
|
534
|
+
"--spec",
|
|
535
|
+
str(deck_spec),
|
|
536
|
+
"--out",
|
|
537
|
+
str(out_file),
|
|
538
|
+
]
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
assert build_payload["result"]["summary"]["notes_slides"] == 1
|
|
542
|
+
generated = Presentation(str(out_file))
|
|
543
|
+
notes_frame = generated.slides[1].notes_slide.notes_text_frame
|
|
544
|
+
assert notes_frame is not None
|
|
545
|
+
assert [paragraph.text for paragraph in notes_frame.paragraphs] == [
|
|
546
|
+
"Lead with the governance angle.",
|
|
547
|
+
"Mention change control",
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def test_slide_create_rejects_conflicting_speaker_notes_inputs(
|
|
552
|
+
template_path: Path,
|
|
553
|
+
manifest_dir: Path,
|
|
554
|
+
tmp_path: Path,
|
|
555
|
+
) -> None:
|
|
556
|
+
out_file = tmp_path / "slide-conflicting-notes.pptx"
|
|
557
|
+
notes_file = tmp_path / "speaker-notes.txt"
|
|
558
|
+
notes_file.write_text("Use one notes input only.", encoding="utf-8")
|
|
559
|
+
_init_manifest(template_path, manifest_dir)
|
|
560
|
+
|
|
561
|
+
result = runner.invoke(
|
|
562
|
+
app,
|
|
563
|
+
[
|
|
564
|
+
"slide",
|
|
565
|
+
"create",
|
|
566
|
+
"--manifest",
|
|
567
|
+
str(manifest_dir),
|
|
568
|
+
"--layout",
|
|
569
|
+
"title-only",
|
|
570
|
+
"--set",
|
|
571
|
+
"title=Quarterly Update",
|
|
572
|
+
"--notes",
|
|
573
|
+
"Inline notes",
|
|
574
|
+
"--notes-file",
|
|
575
|
+
str(notes_file),
|
|
576
|
+
"--out",
|
|
577
|
+
str(out_file),
|
|
578
|
+
"--format",
|
|
579
|
+
"json",
|
|
580
|
+
],
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
assert result.exit_code == 10
|
|
584
|
+
payload = json.loads(result.stdout)
|
|
585
|
+
assert payload["errors"][0]["code"] == "ERR_VALIDATION_INPUT"
|
|
586
|
+
|
|
587
|
+
|
|
412
588
|
def test_deck_build_auto_detects_markdown_bullets_and_preserves_text_opt_out(
|
|
413
589
|
template_path: Path,
|
|
414
590
|
manifest_dir: Path,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/references/excal-diagrams.md
RENAMED
|
File without changes
|
{pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/references/mckinsey-style.md
RENAMED
|
File without changes
|
{pptx_cli-1.2.5 → pptx_cli-1.2.6}/.github/skills/pptx-deck-builder/references/pptx-workflow.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|