pptx-cli 1.2.5__tar.gz → 1.3.0__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.3.0/.claude/settings.local.json +7 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx/SKILL.md +4 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx-deck-builder/SKILL.md +7 -1
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/PKG-INFO +16 -1
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/PRD.md +11 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/README.md +15 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/__init__.py +1 -1
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/cli.py +47 -4
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/compose.py +47 -5
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/guide.py +18 -2
- pptx_cli-1.3.0/src/pptx_cli/commands/schema.py +224 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/composition.py +40 -2
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/models/manifest.py +1 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/tests/test_cli.py +176 -0
- pptx_cli-1.3.0/tests/test_schema.py +80 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.editorconfig +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/copilot-instructions.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/instructions/backend.instructions.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/instructions/testing.instructions.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx/references/deck-spec.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx-deck-builder/references/excal-diagrams.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx-deck-builder/references/mckinsey-style.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx-deck-builder/references/pptx-workflow.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/workflows/ci.yml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/workflows/publish.yml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.gitignore +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/.python-version +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/AGENTS.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/ARCHITECTURE.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/CHANGELOG.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/CLI-MANIFEST.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/CONTRIBUTING.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/DECISIONS/ADR-0001-initial-architecture.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/LICENSE +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/PREVIEW-FUTURE.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/PROJECT.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/SCAFFOLD.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/SECURITY.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/TESTING.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/docs/DOMAIN.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/docs/GLOSSARY.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/docs/ROADMAP.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/docs/SCAFFOLDING-NOTES.md +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/guardrails/.gitignore +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/guardrails/guardrails.jsonl +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/guardrails/links.jsonl +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/guardrails/references.jsonl +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/guardrails/taxonomy.json +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/guardrails-explorer.html +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/pyproject.toml +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/pyrightconfig.json +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/scripts/bump_version.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/__main__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/__init__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/init.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/inspect.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/manifest_ops.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/validate.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/commands/wrapper.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/__init__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/ids.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/io.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/manifest_store.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/markdown.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/runtime.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/template.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/validation.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/core/versioning.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/models/__init__.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/src/pptx_cli/models/envelope.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/tests/conftest.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/tests/test_versioning.py +0 -0
- {pptx_cli-1.2.5 → pptx_cli-1.3.0}/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.
|
|
3
|
+
Version: 1.3.0
|
|
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
|
|
@@ -20,6 +20,7 @@ from pptx_cli.commands.inspect import (
|
|
|
20
20
|
show_theme,
|
|
21
21
|
)
|
|
22
22
|
from pptx_cli.commands.manifest_ops import manifest_diff, manifest_schema
|
|
23
|
+
from pptx_cli.commands.schema import build_schema_document, copy_to_clipboard
|
|
23
24
|
from pptx_cli.commands.validate import validate_command
|
|
24
25
|
from pptx_cli.commands.wrapper import wrapper_generate
|
|
25
26
|
from pptx_cli.core.composition import CompositionError
|
|
@@ -30,8 +31,8 @@ from pptx_cli.models.envelope import CliMessage, Envelope, Metrics
|
|
|
30
31
|
app = typer.Typer(
|
|
31
32
|
help=(
|
|
32
33
|
"Template-bound PowerPoint generation for enterprise decks. Supports text, "
|
|
33
|
-
"images, tables, charts,
|
|
34
|
-
"placeholders."
|
|
34
|
+
"images, tables, charts, markdown-text content in template-approved "
|
|
35
|
+
"placeholders, and optional per-slide speaker notes."
|
|
35
36
|
),
|
|
36
37
|
no_args_is_help=True,
|
|
37
38
|
)
|
|
@@ -41,7 +42,10 @@ theme_app = typer.Typer(help="Inspect extracted theme metadata.")
|
|
|
41
42
|
assets_app = typer.Typer(help="Inspect extracted asset references.")
|
|
42
43
|
slide_app = typer.Typer(help="Create slides from approved layouts.")
|
|
43
44
|
deck_app = typer.Typer(
|
|
44
|
-
help=
|
|
45
|
+
help=(
|
|
46
|
+
"Build full decks from structured specs, including markdown-text content "
|
|
47
|
+
"and optional per-slide speaker notes."
|
|
48
|
+
)
|
|
45
49
|
)
|
|
46
50
|
manifest_app = typer.Typer(help="Work with manifest packages and schemas.")
|
|
47
51
|
wrapper_app = typer.Typer(help="Generate thin template-specific wrapper CLIs.")
|
|
@@ -235,6 +239,27 @@ def guide(format: FormatOption = None) -> None:
|
|
|
235
239
|
execute("guide.show", format, lambda: build_guide_document().model_dump(mode="json"))
|
|
236
240
|
|
|
237
241
|
|
|
242
|
+
@app.command("schema")
|
|
243
|
+
def schema_command(
|
|
244
|
+
template: Annotated[
|
|
245
|
+
Path | None,
|
|
246
|
+
typer.Option("--template", help="Path to a manifest package directory."),
|
|
247
|
+
] = None,
|
|
248
|
+
no_copy: Annotated[
|
|
249
|
+
bool,
|
|
250
|
+
typer.Option("--no-copy", help="Skip copying output to the clipboard."),
|
|
251
|
+
] = False,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Print the deck-spec YAML reference (for pasting into LLM prompts)."""
|
|
254
|
+
text = build_schema_document(template)
|
|
255
|
+
typer.echo(text)
|
|
256
|
+
if not no_copy:
|
|
257
|
+
if copy_to_clipboard(text):
|
|
258
|
+
typer.echo("(copied to clipboard)", err=True)
|
|
259
|
+
else:
|
|
260
|
+
typer.echo("(clipboard copy failed – install xclip or pipe manually)", err=True)
|
|
261
|
+
|
|
262
|
+
|
|
238
263
|
@app.command("init")
|
|
239
264
|
def init_command(
|
|
240
265
|
template: Annotated[Path, typer.Argument(help="Path to the source .pptx template")],
|
|
@@ -338,6 +363,20 @@ def slide_create_command(
|
|
|
338
363
|
),
|
|
339
364
|
),
|
|
340
365
|
] = None,
|
|
366
|
+
notes: Annotated[
|
|
367
|
+
str | None,
|
|
368
|
+
typer.Option(
|
|
369
|
+
"--notes",
|
|
370
|
+
help="Speaker notes text for the slide. Markdown-looking multiline text is supported.",
|
|
371
|
+
),
|
|
372
|
+
] = None,
|
|
373
|
+
notes_file: Annotated[
|
|
374
|
+
Path | None,
|
|
375
|
+
typer.Option(
|
|
376
|
+
"--notes-file",
|
|
377
|
+
help="Path to a UTF-8 text or markdown file to use as speaker notes.",
|
|
378
|
+
),
|
|
379
|
+
] = None,
|
|
341
380
|
dry_run: DryRunOption = False,
|
|
342
381
|
overwrite: OverwriteOption = False,
|
|
343
382
|
format: FormatOption = None,
|
|
@@ -346,6 +385,7 @@ def slide_create_command(
|
|
|
346
385
|
|
|
347
386
|
Use --set key=@notes.md or a multiline markdown-looking value to populate
|
|
348
387
|
markdown-text placeholders with headings, lists, and inline emphasis.
|
|
388
|
+
Use --notes or --notes-file for slide-level speaker notes.
|
|
349
389
|
"""
|
|
350
390
|
|
|
351
391
|
execute(
|
|
@@ -356,6 +396,8 @@ def slide_create_command(
|
|
|
356
396
|
layout,
|
|
357
397
|
list(set_values or []),
|
|
358
398
|
out,
|
|
399
|
+
notes=notes,
|
|
400
|
+
notes_file=notes_file,
|
|
359
401
|
dry_run=dry_run,
|
|
360
402
|
overwrite=overwrite,
|
|
361
403
|
),
|
|
@@ -383,7 +425,8 @@ def deck_build_command(
|
|
|
383
425
|
"""Build a deck from a structured spec.
|
|
384
426
|
|
|
385
427
|
Deck specs can provide markdown-text content explicitly or rely on multiline
|
|
386
|
-
markdown-looking strings for headings, lists, and inline emphasis.
|
|
428
|
+
markdown-looking strings for headings, lists, and inline emphasis. Each slide
|
|
429
|
+
may also provide an optional `notes` field for speaker notes.
|
|
387
430
|
"""
|
|
388
431
|
|
|
389
432
|
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
|
|
@@ -17,6 +17,16 @@ def build_guide_document() -> GuideDocument:
|
|
|
17
17
|
mutates=False,
|
|
18
18
|
examples=["pptx guide --format json"],
|
|
19
19
|
),
|
|
20
|
+
GuideCommand(
|
|
21
|
+
id="schema.show",
|
|
22
|
+
summary="Print the deck-spec YAML reference for pasting into LLM prompts",
|
|
23
|
+
mutates=False,
|
|
24
|
+
examples=[
|
|
25
|
+
"pptx schema",
|
|
26
|
+
"pptx schema --template ./corp-template",
|
|
27
|
+
"pptx schema --no-copy",
|
|
28
|
+
],
|
|
29
|
+
),
|
|
20
30
|
GuideCommand(
|
|
21
31
|
id="template.init",
|
|
22
32
|
summary="Initialize a manifest package from a source template",
|
|
@@ -64,12 +74,15 @@ def build_guide_document() -> GuideDocument:
|
|
|
64
74
|
),
|
|
65
75
|
GuideCommand(
|
|
66
76
|
id="slide.create",
|
|
67
|
-
summary="Create a slide from an approved layout",
|
|
77
|
+
summary="Create a slide from an approved layout with optional speaker notes",
|
|
68
78
|
mutates=True,
|
|
69
79
|
input_schema=DeckSpec.model_json_schema(),
|
|
70
80
|
examples=[
|
|
71
81
|
"pptx slide create --manifest ./corp-template --layout title-only "
|
|
72
82
|
"--set title=Hello --out ./out/slide.pptx --dry-run",
|
|
83
|
+
"pptx slide create --manifest ./corp-template --layout title-only "
|
|
84
|
+
"--set title=Hello --notes-file ./speaker-notes.md "
|
|
85
|
+
"--out ./out/slide-with-notes.pptx",
|
|
73
86
|
"pptx slide create --manifest ./corp-template "
|
|
74
87
|
"--layout 3-front-page-title-and-picture "
|
|
75
88
|
"--set title=Workflow --set picture=@out/workflow.png "
|
|
@@ -78,7 +91,7 @@ def build_guide_document() -> GuideDocument:
|
|
|
78
91
|
),
|
|
79
92
|
GuideCommand(
|
|
80
93
|
id="deck.build",
|
|
81
|
-
summary="Build a deck from a structured spec",
|
|
94
|
+
summary="Build a deck from a structured spec with optional per-slide speaker notes",
|
|
82
95
|
mutates=True,
|
|
83
96
|
input_schema=DeckSpec.model_json_schema(),
|
|
84
97
|
examples=[
|
|
@@ -151,6 +164,9 @@ def build_guide_document() -> GuideDocument:
|
|
|
151
164
|
"placeholder_keys": (
|
|
152
165
|
"logical placeholder keys such as title, subtitle, content_1, or picture"
|
|
153
166
|
),
|
|
167
|
+
"slide_notes": (
|
|
168
|
+
"optional per-slide speaker notes via SlideSpec.notes or slide create --notes"
|
|
169
|
+
),
|
|
154
170
|
"manifest_path": "path to a manifest package directory containing manifest.yaml",
|
|
155
171
|
},
|
|
156
172
|
concurrency={
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Build a human-readable YAML reference for deck specs.
|
|
2
|
+
|
|
3
|
+
Designed to be pasted into LLM prompts so the model knows how to
|
|
4
|
+
author valid ``pptx deck build`` input.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from pptx_cli.core.manifest_store import load_effective_manifest
|
|
17
|
+
from pptx_cli.models.manifest import LayoutContract, ManifestDocument
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Clipboard helper
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def copy_to_clipboard(text: str) -> bool:
|
|
24
|
+
"""Copy *text* to the system clipboard. Returns True on success."""
|
|
25
|
+
if sys.platform == "win32":
|
|
26
|
+
cmd = ["clip"]
|
|
27
|
+
elif sys.platform == "darwin":
|
|
28
|
+
cmd = ["pbcopy"]
|
|
29
|
+
else:
|
|
30
|
+
cmd = ["xclip", "-selection", "clipboard"]
|
|
31
|
+
try:
|
|
32
|
+
subprocess.run(
|
|
33
|
+
cmd,
|
|
34
|
+
input=text.encode("utf-8"),
|
|
35
|
+
check=True,
|
|
36
|
+
timeout=5,
|
|
37
|
+
)
|
|
38
|
+
except (FileNotFoundError, subprocess.SubprocessError):
|
|
39
|
+
return False
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Generic (template-free) schema
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
_GENERIC_SCHEMA = """\
|
|
48
|
+
<slide-schema>
|
|
49
|
+
Use this schema to draft presentation content. Layout assignment and
|
|
50
|
+
template binding happen later, so focus on the slides themselves.
|
|
51
|
+
|
|
52
|
+
metadata:
|
|
53
|
+
title: "<deck title>"
|
|
54
|
+
author: "<author name>"
|
|
55
|
+
|
|
56
|
+
slides:
|
|
57
|
+
- title: "Slide heading"
|
|
58
|
+
body: |
|
|
59
|
+
- Bullet one
|
|
60
|
+
- Bullet two
|
|
61
|
+
notes: |
|
|
62
|
+
Talk track for this slide.
|
|
63
|
+
|
|
64
|
+
- title: "Data slide"
|
|
65
|
+
body: |
|
|
66
|
+
Key observations from the quarter.
|
|
67
|
+
table:
|
|
68
|
+
kind: table
|
|
69
|
+
columns: [Col A, Col B]
|
|
70
|
+
rows:
|
|
71
|
+
- [val1, val2]
|
|
72
|
+
- [val3, val4]
|
|
73
|
+
notes: Additional context.
|
|
74
|
+
|
|
75
|
+
- title: "Trend slide"
|
|
76
|
+
body: |
|
|
77
|
+
Revenue grew 50% year-over-year.
|
|
78
|
+
chart:
|
|
79
|
+
kind: chart
|
|
80
|
+
chart_type: column_clustered
|
|
81
|
+
categories: [Q1, Q2, Q3]
|
|
82
|
+
series:
|
|
83
|
+
- name: Revenue
|
|
84
|
+
values: [12, 15, 18]
|
|
85
|
+
|
|
86
|
+
- title: "Visual slide"
|
|
87
|
+
image:
|
|
88
|
+
kind: image
|
|
89
|
+
path: path/to/image.png
|
|
90
|
+
image_fit: fit
|
|
91
|
+
</slide-schema>
|
|
92
|
+
|
|
93
|
+
<content-rules>
|
|
94
|
+
- Each slide MUST have a "title".
|
|
95
|
+
- Use "body" for the main text area. Markdown is auto-detected
|
|
96
|
+
(headings, bullets, **bold**, *italic*).
|
|
97
|
+
- Use "table", "chart", or "image" keys for structured content.
|
|
98
|
+
chart_type accepts: column_clustered, bar_clustered, line, pie.
|
|
99
|
+
image_fit accepts: "fit" (default) or "cover".
|
|
100
|
+
- "notes" is optional speaker-notes text (markdown ok).
|
|
101
|
+
- Do NOT include a "layout" key – layout is assigned later when
|
|
102
|
+
binding to a corporate template.
|
|
103
|
+
</content-rules>
|
|
104
|
+
|
|
105
|
+
<style-guide>
|
|
106
|
+
Follow these principles to produce decision-grade slides.
|
|
107
|
+
|
|
108
|
+
Structure
|
|
109
|
+
- Lead with the answer. Do not build up to the conclusion.
|
|
110
|
+
- Pyramid Principle: governing thought -> supporting arguments -> evidence.
|
|
111
|
+
- Arguments must be MECE (mutually exclusive, collectively exhaustive).
|
|
112
|
+
- One slide = one message. Two insights -> two slides.
|
|
113
|
+
|
|
114
|
+
Titles
|
|
115
|
+
- Use action titles, not topic labels. The title states the takeaway.
|
|
116
|
+
Bad: "Market overview"
|
|
117
|
+
Good: "Nordic retail banking margins will remain under pressure through 2027"
|
|
118
|
+
- Title storyline reads on its own – skim only titles and understand the
|
|
119
|
+
full argument.
|
|
120
|
+
- Formulas:
|
|
121
|
+
Insight: "[What happened] because [driver]"
|
|
122
|
+
Comparison: "[A] outperforms [B] on [criterion]"
|
|
123
|
+
Implication: "[Fact] puts [objective] at risk"
|
|
124
|
+
Recommendation: "[Org] should [action] to achieve [outcome]"
|
|
125
|
+
|
|
126
|
+
Executive summary slide
|
|
127
|
+
- Situation: what context everyone agrees on.
|
|
128
|
+
- Complication: what changed or created urgency.
|
|
129
|
+
- Answer: the recommended response.
|
|
130
|
+
- Support: 2-3 reasons the answer is correct.
|
|
131
|
+
|
|
132
|
+
Deck sequence (typical)
|
|
133
|
+
1. Title page
|
|
134
|
+
2. Executive summary (situation -> complication -> resolution)
|
|
135
|
+
3. Context / problem framing (only enough to orient)
|
|
136
|
+
4. Analysis body (current state -> root causes -> options -> recommendation)
|
|
137
|
+
5. Recommendation (explicit, decision-ready)
|
|
138
|
+
6. Implementation / roadmap
|
|
139
|
+
7. Risks and mitigations
|
|
140
|
+
8. Appendix (source data, benchmarks, methodology)
|
|
141
|
+
|
|
142
|
+
Body content
|
|
143
|
+
- Every claim backed by evidence. The body proves the headline.
|
|
144
|
+
- Quantify. Replace vague adjectives with measured claims.
|
|
145
|
+
- Keep text tight: verbs, short bullets, no filler.
|
|
146
|
+
- Move backup detail to appendix – main story stays focused.
|
|
147
|
+
</style-guide>
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Template-enriched schema
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def _placeholder_summary(ph: Any) -> dict[str, Any]:
|
|
156
|
+
"""Compact summary of a placeholder contract."""
|
|
157
|
+
entry: dict[str, Any] = {
|
|
158
|
+
"types": ph.supported_content_types,
|
|
159
|
+
}
|
|
160
|
+
if ph.required:
|
|
161
|
+
entry["required"] = True
|
|
162
|
+
if ph.guidance_text:
|
|
163
|
+
entry["guidance"] = ph.guidance_text
|
|
164
|
+
cap = ph.estimated_text_capacity
|
|
165
|
+
if cap is not None:
|
|
166
|
+
entry["max_lines"] = cap.max_lines
|
|
167
|
+
return entry
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _layout_section(layout: LayoutContract) -> dict[str, Any]:
|
|
171
|
+
"""Build a dict describing one layout for the reference doc."""
|
|
172
|
+
section: dict[str, Any] = {}
|
|
173
|
+
if layout.description:
|
|
174
|
+
section["description"] = layout.description
|
|
175
|
+
if layout.aliases:
|
|
176
|
+
section["aliases"] = layout.aliases
|
|
177
|
+
placeholders: dict[str, Any] = {}
|
|
178
|
+
for ph in layout.placeholders:
|
|
179
|
+
placeholders[ph.logical_name] = _placeholder_summary(ph)
|
|
180
|
+
section["placeholders"] = placeholders
|
|
181
|
+
return section
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _example_slide(layout: LayoutContract) -> dict[str, Any]:
|
|
185
|
+
"""Generate an example slide entry using real placeholder names."""
|
|
186
|
+
content: dict[str, str] = {}
|
|
187
|
+
for ph in layout.placeholders:
|
|
188
|
+
if "image" in ph.supported_content_types and "text" not in ph.supported_content_types:
|
|
189
|
+
content[ph.logical_name] = "{ kind: image, path: path/to/image.png }"
|
|
190
|
+
else:
|
|
191
|
+
content[ph.logical_name] = f"<{ph.logical_name} text>"
|
|
192
|
+
slide: dict[str, Any] = {"layout": layout.id, "content": content}
|
|
193
|
+
return slide
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _build_template_section(manifest: ManifestDocument) -> str:
|
|
197
|
+
"""Return the template-specific portion of the reference doc."""
|
|
198
|
+
layouts_ref: dict[str, Any] = {}
|
|
199
|
+
examples: list[dict[str, Any]] = []
|
|
200
|
+
|
|
201
|
+
for layout in manifest.layouts:
|
|
202
|
+
layouts_ref[layout.id] = _layout_section(layout)
|
|
203
|
+
examples.append(_example_slide(layout))
|
|
204
|
+
|
|
205
|
+
doc: dict[str, Any] = {
|
|
206
|
+
"template": manifest.template.name,
|
|
207
|
+
"layouts": layouts_ref,
|
|
208
|
+
"example_slides": examples,
|
|
209
|
+
}
|
|
210
|
+
return yaml.safe_dump(doc, sort_keys=False, allow_unicode=True, width=120)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# Public API
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def build_schema_document(template_dir: Path | None = None) -> str:
|
|
218
|
+
"""Return the full reference document as a string."""
|
|
219
|
+
parts = [_GENERIC_SCHEMA]
|
|
220
|
+
if template_dir is not None:
|
|
221
|
+
manifest = load_effective_manifest(template_dir)
|
|
222
|
+
parts.append("\n# ── Template-specific layouts ──────────────────────────────────\n")
|
|
223
|
+
parts.append(_build_template_section(manifest))
|
|
224
|
+
return "\n".join(parts)
|
|
@@ -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,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from pptx_cli.cli import app
|
|
8
|
+
from pptx_cli.commands.schema import build_schema_document
|
|
9
|
+
|
|
10
|
+
runner = CliRunner()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_generic_schema_contains_deck_structure() -> None:
|
|
14
|
+
text = build_schema_document()
|
|
15
|
+
assert "<slide-schema>" in text
|
|
16
|
+
assert "metadata:" in text
|
|
17
|
+
assert "slides:" in text
|
|
18
|
+
assert "title:" in text
|
|
19
|
+
assert "body:" in text
|
|
20
|
+
assert "notes:" in text
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_generic_schema_documents_content_types() -> None:
|
|
24
|
+
text = build_schema_document()
|
|
25
|
+
assert "kind: image" in text
|
|
26
|
+
assert "kind: table" in text
|
|
27
|
+
assert "kind: chart" in text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_generic_schema_uses_xml_sections() -> None:
|
|
31
|
+
text = build_schema_document()
|
|
32
|
+
assert "<slide-schema>" in text
|
|
33
|
+
assert "</slide-schema>" in text
|
|
34
|
+
assert "<content-rules>" in text
|
|
35
|
+
assert "</content-rules>" in text
|
|
36
|
+
assert "<style-guide>" in text
|
|
37
|
+
assert "</style-guide>" in text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_generic_schema_omits_layout() -> None:
|
|
41
|
+
text = build_schema_document()
|
|
42
|
+
assert "Do NOT include" in text
|
|
43
|
+
assert "layout is assigned later" in text
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_schema_command_outputs_text(tmp_path: str) -> None:
|
|
47
|
+
result = runner.invoke(app, ["schema", "--no-copy"])
|
|
48
|
+
assert result.exit_code == 0
|
|
49
|
+
assert "metadata:" in result.stdout
|
|
50
|
+
assert "slides:" in result.stdout
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_schema_command_copies_to_clipboard() -> None:
|
|
54
|
+
with patch("pptx_cli.cli.copy_to_clipboard", return_value=True) as mock_copy:
|
|
55
|
+
result = runner.invoke(app, ["schema"])
|
|
56
|
+
assert result.exit_code == 0
|
|
57
|
+
mock_copy.assert_called_once()
|
|
58
|
+
assert "copied to clipboard" in result.stderr
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_schema_command_no_copy_skips_clipboard() -> None:
|
|
62
|
+
with patch("pptx_cli.cli.copy_to_clipboard") as mock_copy:
|
|
63
|
+
result = runner.invoke(app, ["schema", "--no-copy"])
|
|
64
|
+
assert result.exit_code == 0
|
|
65
|
+
mock_copy.assert_not_called()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_schema_with_template(template_path: str, manifest_dir: str) -> None:
|
|
69
|
+
# First init a manifest so we can point --template at it
|
|
70
|
+
init_result = runner.invoke(
|
|
71
|
+
app,
|
|
72
|
+
["init", str(template_path), "--out", str(manifest_dir), "--format", "json"],
|
|
73
|
+
)
|
|
74
|
+
assert init_result.exit_code == 0
|
|
75
|
+
|
|
76
|
+
result = runner.invoke(app, ["schema", "--template", str(manifest_dir), "--no-copy"])
|
|
77
|
+
assert result.exit_code == 0
|
|
78
|
+
assert "template:" in result.stdout
|
|
79
|
+
assert "layouts:" in result.stdout
|
|
80
|
+
assert "placeholders:" in result.stdout
|
|
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.3.0}/.github/skills/pptx-deck-builder/references/excal-diagrams.md
RENAMED
|
File without changes
|
{pptx_cli-1.2.5 → pptx_cli-1.3.0}/.github/skills/pptx-deck-builder/references/mckinsey-style.md
RENAMED
|
File without changes
|
{pptx_cli-1.2.5 → pptx_cli-1.3.0}/.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
|