@qvac/skills 0.0.0 → 0.1.1
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.
- package/LICENSE.md +198 -0
- package/README.md +40 -0
- package/index.d.ts +4 -0
- package/index.js +9 -0
- package/package.json +87 -1
- package/skills/apple-notes/SKILL.md +92 -0
- package/skills/apple-notes/append-note.applescript +9 -0
- package/skills/apple-notes/cli.schema.json +32 -0
- package/skills/apple-notes/create-note.applescript +15 -0
- package/skills/apple-notes/delete-note.applescript +10 -0
- package/skills/apple-notes/edit-note.applescript +10 -0
- package/skills/apple-notes/read-note.applescript +28 -0
- package/skills/apple-notes/references/read.md +65 -0
- package/skills/apple-notes/references/write.md +105 -0
- package/skills/apple-notes/search-notes.applescript +21 -0
- package/skills/apple-reminders/SKILL.md +129 -0
- package/skills/apple-reminders/cli.schema.json +201 -0
- package/skills/apple-reminders/references/edit.md +69 -0
- package/skills/apple-reminders/references/view.md +58 -0
- package/skills/asana/SKILL.md +59 -0
- package/skills/diagrams/SKILL.md +107 -0
- package/skills/diagrams/references/class.md +29 -0
- package/skills/diagrams/references/er.md +27 -0
- package/skills/diagrams/references/flowchart.md +33 -0
- package/skills/diagrams/references/gantt.md +38 -0
- package/skills/diagrams/references/mindmap.md +35 -0
- package/skills/diagrams/references/pie.md +27 -0
- package/skills/diagrams/references/sequence.md +32 -0
- package/skills/diagrams/references/state.md +30 -0
- package/skills/diagrams/references/timeline.md +28 -0
- package/skills/excel/SKILL.md +120 -0
- package/skills/excel/references/create.md +374 -0
- package/skills/excel/references/edit.md +353 -0
- package/skills/excel/references/read.md +99 -0
- package/skills/github/SKILL.md +42 -0
- package/skills/gmail/SKILL.md +142 -0
- package/skills/gmail/operations.json +71 -0
- package/skills/google-calendar/SKILL.md +139 -0
- package/skills/google-calendar/operations.json +62 -0
- package/skills/google-docs/SKILL.md +74 -0
- package/skills/google-docs/operations.json +61 -0
- package/skills/google-docs/references/create.md +97 -0
- package/skills/google-docs/references/edit.md +146 -0
- package/skills/google-docs/references/read.md +49 -0
- package/skills/google-drive/SKILL.md +118 -0
- package/skills/google-drive/operations.json +40 -0
- package/skills/google-sheets/SKILL.md +71 -0
- package/skills/google-sheets/operations.json +85 -0
- package/skills/google-sheets/references/create.md +54 -0
- package/skills/google-sheets/references/edit.md +124 -0
- package/skills/google-sheets/references/read.md +74 -0
- package/skills/image-generation/SKILL.md +48 -0
- package/skills/music-generation/SKILL.md +76 -0
- package/skills/notion/SKILL.md +61 -0
- package/skills/notion/operations.json +53 -0
- package/skills/notion/references/comments.md +65 -0
- package/skills/notion/references/databases.md +68 -0
- package/skills/notion/references/pages.md +119 -0
- package/skills/notion/references/tasks.md +28 -0
- package/skills/obsidian/SKILL.md +122 -0
- package/skills/obsidian/cli.schema.json +392 -0
- package/skills/obsidian/references/read.md +79 -0
- package/skills/obsidian/references/write.md +67 -0
- package/skills/pdf/SKILL.md +110 -0
- package/skills/pdf/references/create.md +169 -0
- package/skills/pdf/references/transform.md +270 -0
- package/skills/pdf/scripts/decrypt.py +26 -0
- package/skills/pdf/scripts/encrypt.py +25 -0
- package/skills/pdf/scripts/extract_text.py +25 -0
- package/skills/pdf/scripts/merge.py +21 -0
- package/skills/pdf/scripts/rotate.py +27 -0
- package/skills/presentations/SKILL.md +118 -0
- package/skills/presentations/references/create.md +399 -0
- package/skills/presentations/references/edit.md +314 -0
- package/skills/presentations/references/read.md +127 -0
- package/skills/spotify/SKILL.md +86 -0
- package/skills/weather/SKILL.md +33 -0
- package/skills/word/SKILL.md +141 -0
- package/skills/word/references/create.md +368 -0
- package/skills/word/references/edit.md +704 -0
- package/skills/word/references/read.md +141 -0
- package/skills/word/references/replace.md +86 -0
- package/skills/word/scripts/list_paragraphs.py +19 -0
- package/skills/word/scripts/replace_paragraphs.py +58 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: presentations
|
|
3
|
+
description: Create, edit, or read PowerPoint (.pptx) decks with python-pptx — deliver decks as chat attachments, or read an attached one to summarize it or answer questions in the chat. Can embed images the user attached to the chat as well as images generated in it.
|
|
4
|
+
tools: [exec(python)]
|
|
5
|
+
platform: [darwin, linux, win32]
|
|
6
|
+
metadata:
|
|
7
|
+
{
|
|
8
|
+
"openclaw":
|
|
9
|
+
{
|
|
10
|
+
"setup":
|
|
11
|
+
{
|
|
12
|
+
"summary": "Runs python-pptx in the in-process Python runtime, from packages that ship with the app. The first use waits for the runtime to start."
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Presentations
|
|
19
|
+
|
|
20
|
+
Build, edit, or read `.pptx` decks by running python-pptx through the `exec`
|
|
21
|
+
tool with `language: "python"`. Declare a produced deck in `outputs` and it
|
|
22
|
+
comes back as a chat attachment the user can save — exactly like a
|
|
23
|
+
`generate_image` result, the `exec` result carries
|
|
24
|
+
`attachments: [{ attachmentId, fileName, byteLength }]`. To answer *from* a
|
|
25
|
+
deck instead of building one, run a read call alone — no `outputs` — and reply
|
|
26
|
+
in the chat.
|
|
27
|
+
|
|
28
|
+
## Load the Recipe File First
|
|
29
|
+
|
|
30
|
+
This file contains no Python. The working recipes live in three reference
|
|
31
|
+
files — load the one for the job with the `skill` tool BEFORE writing any
|
|
32
|
+
Python, then copy its recipe and change the content:
|
|
33
|
+
|
|
34
|
+
Each load is a real `skill` tool call — printing the call as JSON or text in
|
|
35
|
+
your reply loads nothing.
|
|
36
|
+
|
|
37
|
+
- **Building a new deck** (no existing deck involved; includes embedding
|
|
38
|
+
images generated or uploaded in this chat): call the `skill` tool with
|
|
39
|
+
`name: "presentations"` and `file: "references/create.md"`.
|
|
40
|
+
- **Editing a deck already in this chat** (retitle, recolor, add or revise
|
|
41
|
+
slides on a deck the user attached or a prior call built): call the `skill`
|
|
42
|
+
tool with `name: "presentations"` and `file: "references/edit.md"`.
|
|
43
|
+
- **Answering from an attached deck** (a summary, a question answered,
|
|
44
|
+
content pulled into the chat — no file produced): call the `skill` tool
|
|
45
|
+
with `name: "presentations"` and `file: "references/read.md"`.
|
|
46
|
+
- **A summary delivered as a new file** is a read followed by a build: load
|
|
47
|
+
both `references/read.md` and `references/create.md`.
|
|
48
|
+
|
|
49
|
+
Never write the Python from memory. The recipes carry required rules (typed
|
|
50
|
+
`Inches`/`Pt` lengths, layout reuse, the read-before-edit flow, exact version
|
|
51
|
+
pins, attachment staging) that fail in non-obvious ways when improvised;
|
|
52
|
+
loading the file is one cheap read-only call.
|
|
53
|
+
|
|
54
|
+
## When to Use
|
|
55
|
+
|
|
56
|
+
- The user asks for a presentation, deck, slides, `.pptx`, PowerPoint, or Keynote-openable file.
|
|
57
|
+
- The user attaches a `.pptx` and asks what it says — a summary, a question
|
|
58
|
+
answered, or content pulled out into the chat.
|
|
59
|
+
- The user wants a slide deck that embeds images generated in this chat.
|
|
60
|
+
- The user attaches an image — a photo, a logo, a screenshot — and wants it on a
|
|
61
|
+
slide. You can see the image, and you can also stage the file it came from:
|
|
62
|
+
load `references/create.md` (new deck) or `references/edit.md` (existing
|
|
63
|
+
deck). Generating a lookalike instead is a failed turn.
|
|
64
|
+
|
|
65
|
+
## When NOT to Use
|
|
66
|
+
|
|
67
|
+
- The user wants markdown or a document in the chat and no deck is involved —
|
|
68
|
+
just write it. Summarizing or answering from an attached `.pptx` **is** this
|
|
69
|
+
skill: load `references/read.md`.
|
|
70
|
+
- The user wants a single image — call `generate_image` alone.
|
|
71
|
+
|
|
72
|
+
## Rules for Every Job
|
|
73
|
+
|
|
74
|
+
**You build it, not the user.** Deliver the deck, never the recipe. Do NOT
|
|
75
|
+
print the python source in chat, do NOT tell the user to install python-pptx,
|
|
76
|
+
run a script, or open a terminal — they have no terminal in this chat and the
|
|
77
|
+
code would not run there. The deck exists only if an `exec` call with
|
|
78
|
+
`outputs` succeeds and returns the attachment. Falling back to "here is the
|
|
79
|
+
script, run it yourself" is a failed turn, and so is writing slides as
|
|
80
|
+
markdown/chat text instead of the `.pptx` the user asked for — answering a
|
|
81
|
+
*read* request as chat text is the read flow's finish, not this failure.
|
|
82
|
+
|
|
83
|
+
**Success = stop.** When `exitCode` is `0` and `attachments` lists the
|
|
84
|
+
`.pptx`, the deck is done. Do not call `exec` again for the same request —
|
|
85
|
+
not to "confirm", not to "improve", not a second identical build; a second
|
|
86
|
+
deck attach is rejected. Exactly one successful **build** `exec` per deck
|
|
87
|
+
request — a no-`outputs` read delivers nothing and is not one of them: it
|
|
88
|
+
belongs before the build on an edit, never after it, and on a read request it
|
|
89
|
+
stands alone. Reply with a single line: file name + slide count from stdout.
|
|
90
|
+
If the result has `missingOutputs` instead, the file was never written: check
|
|
91
|
+
the `save()` name matches the declared output and rerun once.
|
|
92
|
+
|
|
93
|
+
**Failures are fixed in the code, not around it.** If a run fails, fix the
|
|
94
|
+
Python against the loaded reference file's recipes and errors table and call
|
|
95
|
+
`exec` again. If two consecutive calls fail with the same error, the fix from
|
|
96
|
+
the first attempt did not land — re-read the traceback line-by-line before a
|
|
97
|
+
third call; retrying the identical `command`, or a version with only cosmetic
|
|
98
|
+
changes, is a loop, not a fix. An error in your code is never a fault in
|
|
99
|
+
python-pptx or in the runtime: do not switch package pins or hunt a "more
|
|
100
|
+
compatible" version — keep `packages: ["python-pptx==1.0.2"]` — do not wrap
|
|
101
|
+
source in `python -c "…"`, `python3`, `pip`, or shell, and do not "debug" with
|
|
102
|
+
`os.listdir`, `print`, or a non-deck script while `outputs` still lists the
|
|
103
|
+
deck. Never search the web about an error; the answer is always in the result
|
|
104
|
+
you already have.
|
|
105
|
+
|
|
106
|
+
**The runtime is sealed.** There is no shell — `ls`, `cat` and `file` raise
|
|
107
|
+
`SyntaxError`, because the `command` is Python source — and no filesystem to
|
|
108
|
+
check outside the `exec` result. There is no network: `requests`, `urllib`,
|
|
109
|
+
and `socket` all fail, and `http_request` returns truncated text, never image
|
|
110
|
+
bytes — an image behind a URL cannot be downloaded; ask the user to attach it
|
|
111
|
+
or offer `generate_image`. The working directory starts empty on every call:
|
|
112
|
+
a file from an earlier call is gone unless staged again by its attachment id,
|
|
113
|
+
and a file you write but do not declare in `outputs` is discarded.
|
|
114
|
+
|
|
115
|
+
**Never overwrite a staged input.** An edit always saves under a new,
|
|
116
|
+
version-bumped name — `deck.pptx` → `deck-v2.pptx` — and a corrected rebuild
|
|
117
|
+
after a broken delivery goes out under the next version, never the name
|
|
118
|
+
already attached this turn.
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# Building a New Deck (python-pptx)
|
|
2
|
+
|
|
3
|
+
Create a new `.pptx` from scratch by running Python through the `exec` tool.
|
|
4
|
+
A new deck needs **no** `inputs` — do not invent attachment ids. **Exactly
|
|
5
|
+
one** `exec` call per user request when that call succeeds.
|
|
6
|
+
|
|
7
|
+
**A deck that already exists in this chat is never rebuilt here.** "Add a
|
|
8
|
+
slide", "change a title", "revise the deck" — any request that starts from an
|
|
9
|
+
existing `.pptx` is an EDIT: load `references/edit.md` and stage the deck by
|
|
10
|
+
its `attachmentId`. Building a fresh deck for an edit request throws away
|
|
11
|
+
every slide the user already has.
|
|
12
|
+
|
|
13
|
+
## The exec call
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"language": "python",
|
|
18
|
+
"packages": ["python-pptx==1.0.2"],
|
|
19
|
+
"outputs": ["deck.pptx"],
|
|
20
|
+
"command": "..."
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `language` (required) — always `"python"`.
|
|
25
|
+
- `packages` (required) — `["python-pptx==1.0.2"]` on every call. Pin the
|
|
26
|
+
version; an unpinned install resolves a potentially different library
|
|
27
|
+
version. This exact version ships with the app and installs with no network;
|
|
28
|
+
any other version has to be downloaded, which fails on a device that is
|
|
29
|
+
offline.
|
|
30
|
+
- `outputs` — `["deck.pptx"]`. `save("deck.pptx")` must match the declared
|
|
31
|
+
output name exactly. A file you write but do not declare here is discarded.
|
|
32
|
+
- `command` — the multi-line Python source, with real newline characters.
|
|
33
|
+
Never collapse it to one line joined by `;` — a `for`/`if`/`with` after a
|
|
34
|
+
semicolon is a `SyntaxError`. `command` is the program: its first line is
|
|
35
|
+
the first line of Python that runs. There is no shell and no interpreter to
|
|
36
|
+
invoke, and no installer — packages are declared in `packages`.
|
|
37
|
+
- No `inputs` key at all for a new deck.
|
|
38
|
+
- `maxOutputChars` (stdout cap, default 8192, max 65536) is never needed on a
|
|
39
|
+
build call — it prints one line.
|
|
40
|
+
|
|
41
|
+
## Embedding images
|
|
42
|
+
|
|
43
|
+
Two kinds of image input, told apart by where the image came from:
|
|
44
|
+
|
|
45
|
+
**Tool-produced files** (`generate_image` output, a prior deck return): stage
|
|
46
|
+
them with the exact `attachmentId` from the tool result — never placeholders
|
|
47
|
+
like `att_deck`, `att_image`, or any id you made up.
|
|
48
|
+
|
|
49
|
+
**Images the user uploaded** ("use this image", a photo attached to their
|
|
50
|
+
message): there is no id to copy — an uploaded image shows none. Stage them
|
|
51
|
+
with `path` only and **no `attachmentId` key**; the first id-less entry is the
|
|
52
|
+
first image of the user's latest message, the second is its second image, and
|
|
53
|
+
so on — never more id-less entries than that message has images. When it has
|
|
54
|
+
none, a single id-less entry resolves to the chat's most recent image instead.
|
|
55
|
+
|
|
56
|
+
Seeing the image in your context is not the same as staging it: the deck is
|
|
57
|
+
built by Python, which reads the working directory and never your context, so
|
|
58
|
+
an uploaded image reaches a slide only through an id-less `inputs` entry. Do
|
|
59
|
+
not call `generate_image` to recreate what the user attached, and do not tell
|
|
60
|
+
them the image cannot be used — the id-less entry is how it is used.
|
|
61
|
+
|
|
62
|
+
**Files the user uploaded that are not images** (a `.pptx` to revise, any
|
|
63
|
+
document): these *do* show an id, on the `[Attached file "…" — attachmentId:
|
|
64
|
+
…]` line of the message that carried them. Copy it verbatim into
|
|
65
|
+
`attachmentId`, exactly as for a tool-produced file. The id-less form never
|
|
66
|
+
reaches them. (Revising an existing deck is its own flow — load
|
|
67
|
+
`references/edit.md`.)
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"language": "python",
|
|
72
|
+
"packages": ["python-pptx==1.0.2"],
|
|
73
|
+
"inputs": [
|
|
74
|
+
{ "attachmentId": "<id from generate_image or prior deck>", "path": "slide1.png" },
|
|
75
|
+
{ "path": "uploaded.png" }
|
|
76
|
+
],
|
|
77
|
+
"outputs": ["deck.pptx"],
|
|
78
|
+
"command": "..."
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Staged files land in the working directory under the bare `path` names —
|
|
83
|
+
reference `slide.shapes.add_picture("slide1.png", …)` by that name only.
|
|
84
|
+
Paths must be unique bare filenames. The working directory is fresh on every
|
|
85
|
+
call, so a file written by an earlier call is gone unless it is staged again
|
|
86
|
+
as a chat attachment; an attachment from an earlier turn can be used when its
|
|
87
|
+
attachment id is available in the conversation — from a tool result or an
|
|
88
|
+
`[Attached file …]` line — otherwise ask the user to attach the file again.
|
|
89
|
+
|
|
90
|
+
`attachment … not found in this chat` means you invented an id or the file is
|
|
91
|
+
not attached. If the file you meant is an image the user uploaded, drop the
|
|
92
|
+
`attachmentId` key; if it came from a tool result or an `[Attached file …]`
|
|
93
|
+
line, re-copy the exact id; for a new deck drop `inputs` entirely; otherwise
|
|
94
|
+
ask the user to re-attach.
|
|
95
|
+
|
|
96
|
+
If an image was staged in `inputs`, embed it in **that** single build with
|
|
97
|
+
`slide.shapes.add_picture` — never deliver a deck and then rebuild to add the
|
|
98
|
+
image. Soft-failing (`try`/`except` around the picture) and saving without it
|
|
99
|
+
is a failed turn, not a success.
|
|
100
|
+
|
|
101
|
+
## Image URLs do not work — never download
|
|
102
|
+
|
|
103
|
+
Your Python code has **no network access**: `requests`, `urllib`, and `socket`
|
|
104
|
+
all fail with a network error, and `http_request` returns truncated text, never
|
|
105
|
+
image bytes. When the user gives an image URL, do not try to fetch it from Python
|
|
106
|
+
and do not retry through other tools — that is a dead end. Say the link cannot be
|
|
107
|
+
downloaded and ask the user to attach the image itself, or offer `generate_image`
|
|
108
|
+
for a similar visual. Then build the deck with the staged attachment as above.
|
|
109
|
+
|
|
110
|
+
Say it in the reply, every time. A deck that quietly ships without the image the
|
|
111
|
+
user linked is a failed turn: they asked for that image, and silence reads as
|
|
112
|
+
though it is on the slide. Name the URL you could not fetch and what you need
|
|
113
|
+
instead.
|
|
114
|
+
|
|
115
|
+
## Writing the Deck
|
|
116
|
+
|
|
117
|
+
Start from this. It is a complete, working deck — a title slide and a bullet slide,
|
|
118
|
+
16:9, every paragraph sized, saved under the declared output name. Copy it and change
|
|
119
|
+
the content; do not assemble a deck from memory.
|
|
120
|
+
|
|
121
|
+
**Keep the source multi-line.** A `for`/`if`/`with` after a semicolon is a
|
|
122
|
+
`SyntaxError` — paste the block with real newlines, not `stmt; for x in y: …`.
|
|
123
|
+
|
|
124
|
+
**Every length is a typed length — `Inches(...)`, `Pt(...)` or `Emu(...)`, never
|
|
125
|
+
a bare number.** This holds for every position and size anywhere in the deck:
|
|
126
|
+
both pairs of `add_textbox(left, top, width, height)`, the `left`/`top` and the
|
|
127
|
+
`width`/`height` of `add_picture(...)`, `prs.slide_width` and `prs.slide_height`,
|
|
128
|
+
table column widths and row heights, and every margin or offset.
|
|
129
|
+
|
|
130
|
+
python-pptx reads a bare number as **EMU**, and there are 914400 EMU to the inch.
|
|
131
|
+
So `add_textbox(0, 0, 12, 0)` is not "12 wide" — it is a box 0.000013in wide and
|
|
132
|
+
0in tall, pinned to the top-left corner. Nothing raises: `exitCode` is `0`, there
|
|
133
|
+
is no traceback and no warning, and the deck is delivered looking broken. Write
|
|
134
|
+
`add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))` instead.
|
|
135
|
+
|
|
136
|
+
Recognise the symptom, because it is the only signal you get: **text crammed
|
|
137
|
+
into the top-left corner, or a box that has no size**, means a raw number reached
|
|
138
|
+
an argument that required a typed length. Do not tune the numbers — wrap them.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from pptx.util import Inches
|
|
142
|
+
|
|
143
|
+
box = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(1.5))
|
|
144
|
+
# NOT add_textbox(1, 1, 8, 2) — that is 8 EMU wide, an invisible box
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Keep every underscore in API names.** `text_frame`, `add_slide`, `slide_layouts`,
|
|
148
|
+
`slide_width`, `word_wrap`, `add_paragraph`, `add_picture`, `add_textbox`, `PP_ALIGN`
|
|
149
|
+
— stripping them to `textframe` / `addslide` / `addpicture` fails. Copy identifiers
|
|
150
|
+
exactly as written below:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from pptx import Presentation
|
|
154
|
+
from pptx.util import Inches, Pt
|
|
155
|
+
from pptx.enum.text import PP_ALIGN
|
|
156
|
+
|
|
157
|
+
prs = Presentation()
|
|
158
|
+
prs.slide_width = Inches(13.333) # 16:9 is not the default
|
|
159
|
+
prs.slide_height = Inches(7.5)
|
|
160
|
+
|
|
161
|
+
# Title slide — layout 0 owns a title and a subtitle
|
|
162
|
+
slide = prs.slides.add_slide(prs.slide_layouts[0])
|
|
163
|
+
slide.shapes.title.text = "Why the Sky Is Blue"
|
|
164
|
+
slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(44)
|
|
165
|
+
subtitle = slide.placeholders[1].text_frame
|
|
166
|
+
subtitle.text = "Rayleigh scattering, in four points"
|
|
167
|
+
subtitle.paragraphs[0].font.size = Pt(24)
|
|
168
|
+
|
|
169
|
+
# Content slide — layout 1 owns a title and a body
|
|
170
|
+
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
|
171
|
+
slide.shapes.title.text = "What Happens"
|
|
172
|
+
slide.shapes.title.text_frame.paragraphs[0].font.size = Pt(36)
|
|
173
|
+
tf = slide.placeholders[1].text_frame
|
|
174
|
+
tf.word_wrap = True
|
|
175
|
+
for index, point in enumerate([
|
|
176
|
+
"Sunlight arrives carrying every visible wavelength",
|
|
177
|
+
"Air molecules scatter short wavelengths hardest",
|
|
178
|
+
"Blue scatters far more than red",
|
|
179
|
+
"So the daytime sky reads blue in every direction",
|
|
180
|
+
]):
|
|
181
|
+
p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()
|
|
182
|
+
p.text = point
|
|
183
|
+
p.font.size = Pt(20)
|
|
184
|
+
|
|
185
|
+
prs.save("deck.pptx") # must match the declared output exactly
|
|
186
|
+
print(f"{len(prs.slides)} slides")
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Layouts `0` and `1` own the placeholders that example writes to.
|
|
190
|
+
|
|
191
|
+
**Choosing a layout has one rule, and it depends on where the deck came from:**
|
|
192
|
+
|
|
193
|
+
- **Adding to a deck the user gave you** — reuse the layout its own slides
|
|
194
|
+
already use: read a comparable existing slide and pass its `.slide_layout` to
|
|
195
|
+
`add_slide`. That is the only way the new slide inherits the deck's theme.
|
|
196
|
+
That flow is `references/edit.md` — load it.
|
|
197
|
+
- **Building a new deck** — index the bundled template, which commonly uses `0`
|
|
198
|
+
(title), `1` (title + content), `5` (title only), and `6` (blank). Those
|
|
199
|
+
indices belong to *that* template and mean nothing on an uploaded deck.
|
|
200
|
+
|
|
201
|
+
A layout only owns the placeholders it declares, and python-pptx returns `None` for
|
|
202
|
+
the rest — on the blank layout `shapes.title` is `None`, so `shapes.title.text = …`
|
|
203
|
+
raises `AttributeError: 'NoneType' object has no attribute 'text'`, and
|
|
204
|
+
`placeholders[1]` raises `KeyError`. So each slide is one of exactly two kinds, never
|
|
205
|
+
a mix:
|
|
206
|
+
|
|
207
|
+
| slide kind | layout | how you write text |
|
|
208
|
+
| --- | --- | --- |
|
|
209
|
+
| title / title + body | the deck's own layout, or `0`, `1`, `5` in a new deck | `shapes.title`, `placeholders[1]` |
|
|
210
|
+
| hand-designed | `6` (blank) | `shapes.add_textbox(...)` for **every** box, title included |
|
|
211
|
+
|
|
212
|
+
On layout `6` there is no title to reach for — the title is a text box you add.
|
|
213
|
+
|
|
214
|
+
The blank layout is **not** a co-equal way to write a content slide. It is for a
|
|
215
|
+
slide you are genuinely designing by hand — a full-bleed image, a diagram, a
|
|
216
|
+
custom split — and it inherits no font, size, colour or position from the
|
|
217
|
+
template. Using it plus `add_textbox` to hold ordinary title-and-bullets content
|
|
218
|
+
on a deck the user uploaded produces a slide that visibly does not belong: wrong
|
|
219
|
+
typeface, wrong sizes, wrong margins, and no bullets. Placeholders exist so you
|
|
220
|
+
do not have to reproduce a theme you cannot see.
|
|
221
|
+
|
|
222
|
+
Bullets — set `tf.text` for the first bullet, then `add_paragraph()` for the rest.
|
|
223
|
+
Using `add_paragraph()` for the first one leaves a blank leading line.
|
|
224
|
+
|
|
225
|
+
A new text frame holds exactly **one** paragraph, and `tf.paragraphs` is a tuple, so
|
|
226
|
+
`tf.paragraphs[1]` raises `IndexError: tuple index out of range` until you have added
|
|
227
|
+
it. Grow the frame with `p = tf.add_paragraph()`, which returns the new paragraph, and
|
|
228
|
+
write through that. A paragraph owns `.text`, `.font` and `.alignment` and nothing
|
|
229
|
+
else — it has no `.paragraphs` and no `.add_paragraph()`, so never reassign your frame
|
|
230
|
+
variable to a paragraph.
|
|
231
|
+
|
|
232
|
+
The template's body placeholder inherits 28pt, so size every paragraph you add —
|
|
233
|
+
including sub-levels — or it renders far larger than intended:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from pptx.util import Pt
|
|
237
|
+
|
|
238
|
+
slide = prs.slides.add_slide(prs.slide_layouts[1])
|
|
239
|
+
slide.shapes.title.text = "Agenda"
|
|
240
|
+
tf = slide.placeholders[1].text_frame
|
|
241
|
+
tf.word_wrap = True
|
|
242
|
+
for index, point in enumerate(["first point", "second point"]):
|
|
243
|
+
p = tf.paragraphs[0] if index == 0 else tf.add_paragraph()
|
|
244
|
+
p.text = point
|
|
245
|
+
p.font.size = Pt(20) # an unsized paragraph inherits 28pt
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Every deck returned through `outputs` is fitted before it reaches the user, so text
|
|
249
|
+
that would overflow its box is shrunk to fit automatically. That is a safety net, not
|
|
250
|
+
a licence to overfill: shrinking below about 16pt is unreadable from a room. Budget
|
|
251
|
+
each slide at no more than five bullets of about 100 characters. A sixth bullet is a
|
|
252
|
+
second slide titled `... (cont.)`, never a smaller font — and prose belongs in the
|
|
253
|
+
chat reply, not on a slide.
|
|
254
|
+
|
|
255
|
+
Free text — the only way to add text outside a placeholder is
|
|
256
|
+
`shapes.add_textbox(left, top, width, height)` — all four are required, and all
|
|
257
|
+
four are typed lengths, never bare numbers — then write into its `.text_frame`.
|
|
258
|
+
There is no `add_text_frame`, no `add_text`, and no `add_paragraph` on `shapes`:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
262
|
+
box = slide.shapes.add_textbox(Inches(0.75), Inches(0.5), Inches(11.83), Inches(1.2))
|
|
263
|
+
tf = box.text_frame
|
|
264
|
+
tf.word_wrap = True
|
|
265
|
+
tf.text = "Why the sky is blue"
|
|
266
|
+
tf.paragraphs[0].font.size = Pt(40) # from pptx.util import Pt
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Text always lives on the `.text_frame`, never on the shape: `box.paragraphs`,
|
|
270
|
+
`box.add_paragraph()`, `box.word_wrap`, and `box.font` all raise AttributeError. Go
|
|
271
|
+
through `tf = box.text_frame` first — `tf.text`, `tf.paragraphs[0]`,
|
|
272
|
+
`tf.add_paragraph()`, `tf.word_wrap`. `shape.text` is the one shortcut that reads
|
|
273
|
+
through to the frame; there is no matching `shape.font`.
|
|
274
|
+
|
|
275
|
+
Formatting lives one level lower still — on a paragraph or a run, never on a shape or
|
|
276
|
+
a frame. A title is sized through its paragraph:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
title = slide.shapes.title
|
|
280
|
+
title.text = "Why the Sky is Blue"
|
|
281
|
+
title.text_frame.paragraphs[0].font.size = Pt(44) # not title.font.size
|
|
282
|
+
title.text_frame.paragraphs[0].font.bold = True
|
|
283
|
+
title.text_frame.paragraphs[0].alignment = PP_ALIGN.CENTER
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Slides are added with `prs.slides.add_slide(layout)` — `prs.add_slide` does not
|
|
287
|
+
exist. Alignment comes from an enum import, not an attribute path:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
from pptx.enum.text import PP_ALIGN
|
|
291
|
+
|
|
292
|
+
tf.paragraphs[0].alignment = PP_ALIGN.CENTER
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Bullet characters are not needed — a placeholder body renders bullets itself. Give
|
|
296
|
+
each bullet its own paragraph; never pack several `\n`-joined bullets into one.
|
|
297
|
+
Never type the marker into the text: `"1. "`, `"2. "`, `"- "` and `"• "` prefixes
|
|
298
|
+
render *next to* the bullet the placeholder already draws, in the wrong font.
|
|
299
|
+
Bullets belong in a body placeholder for exactly this reason — a bare
|
|
300
|
+
`add_textbox` draws none, and typing them by hand to compensate is the wrong fix.
|
|
301
|
+
Put the content in a placeholder instead.
|
|
302
|
+
|
|
303
|
+
Font color — the type is `RGBColor` with **RGB in all caps**. Not `RgbColor`,
|
|
304
|
+
`rgbColor`, or `rgb_color`:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from pptx.dml.color import RGBColor
|
|
308
|
+
|
|
309
|
+
title.text_frame.paragraphs[0].font.color.rgb = RGBColor(0x1A, 0x73, 0xE8)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Backgrounds and fills — `background` hangs off the slide itself, never off
|
|
313
|
+
`slide.shapes` or a shape, and the fill is set in two steps: `solid()` first,
|
|
314
|
+
then the color:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
from pptx.dml.color import RGBColor
|
|
318
|
+
|
|
319
|
+
fill = slide.background.fill # slide.shapes has no background
|
|
320
|
+
fill.solid()
|
|
321
|
+
fill.fore_color.rgb = RGBColor(0x0B, 0x1F, 0x3A)
|
|
322
|
+
|
|
323
|
+
box.fill.solid() # a shape is tinted through its own .fill
|
|
324
|
+
box.fill.fore_color.rgb = RGBColor(0xF2, 0xF2, 0xF2)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
A paragraph has no `.fill` at all — coloring text goes through the font,
|
|
328
|
+
`paragraph.font.color.rgb = RGBColor(...)`, as above. `fill` exists on a shape
|
|
329
|
+
and on `slide.background`, nowhere else you will need.
|
|
330
|
+
|
|
331
|
+
Images — go through the shapes collection: `slide.shapes.add_picture(...)`. A
|
|
332
|
+
`Slide` has no picture or text-box methods of its own — `slide.add_picture`,
|
|
333
|
+
`slide.addpicture`, `slide.add_textbox`, and `slide.addtextbox` all raise
|
|
334
|
+
`AttributeError: 'Slide' object has no attribute '…'`. Fix: put `.shapes` between
|
|
335
|
+
`slide` and the method. Pass only one of `width`/`height`; passing both distorts
|
|
336
|
+
the picture. Generate image-slide visuals at 1024×512 so they fill the content box;
|
|
337
|
+
a 512×512 square letterboxes with wide empty bands either side.
|
|
338
|
+
|
|
339
|
+
**Do not soft-fail images or imports.** Never wrap `add_picture` or color imports in
|
|
340
|
+
`try`/`except` that prints a warning and continues. A missing file or
|
|
341
|
+
`cannot import name 'RgbColor'` must raise so you fix it and rerun — a deck that
|
|
342
|
+
saves without the requested image is a failed turn, not a success.
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
346
|
+
# correct: slide.shapes.add_picture — never slide.add_picture / slide.addpicture
|
|
347
|
+
slide.shapes.add_picture("slide1.png", Inches(0.75), Inches(1.0), width=Inches(11.83))
|
|
348
|
+
prs.save("deck.pptx")
|
|
349
|
+
print(f"{len(prs.slides)} slides")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Errors
|
|
353
|
+
|
|
354
|
+
- Never print the deck's bytes or base64 — stdout is capped (8 KB by default)
|
|
355
|
+
and the file travels through `outputs`. A build call prints only the slide
|
|
356
|
+
count line (e.g. `7 slides`). No "Presentation created successfully", no
|
|
357
|
+
try/except warnings on stdout.
|
|
358
|
+
- `cannot import name 'RgbColor' from 'pptx.dml.color'` means the name is wrong —
|
|
359
|
+
use `RGBColor` (all-caps RGB). Do not catch the ImportError and save anyway.
|
|
360
|
+
- Never pass an absolute path to `save()`.
|
|
361
|
+
- `SyntaxError: invalid syntax` on a one-line `for`/`if` means the source was
|
|
362
|
+
collapsed — restore multi-line newlines from the sample and rerun. Underscores
|
|
363
|
+
in names (`text_frame`, not `textframe`) must stay. Do not switch to `python -c`
|
|
364
|
+
or change the package pin.
|
|
365
|
+
- `outputs declare a .pptx but command does not build one` means the source never
|
|
366
|
+
calls `Presentation(...).save(...)` — paste the skill sample (edited for content),
|
|
367
|
+
not a diagnostic `os.listdir` or shell wrapper.
|
|
368
|
+
- `ModuleNotFoundError: No module named 'pptx'` means `packages` was missing or
|
|
369
|
+
wrong — add `["python-pptx==1.0.2"]` and rerun. Never try to install it.
|
|
370
|
+
- `attachment … not found in this chat` means `inputs` listed an id that is not in
|
|
371
|
+
this chat (often a copied placeholder like `att_deck`). For a new deck, omit
|
|
372
|
+
`inputs` entirely and rerun. Only stage real ids from prior tool results.
|
|
373
|
+
- Text crammed into the top-left corner, or a shape with no visible size, means a
|
|
374
|
+
bare number reached an argument that required a typed length — python-pptx reads
|
|
375
|
+
it as EMU (914400 to the inch), so `add_textbox(0, 0, 12, 0)` is an invisible box
|
|
376
|
+
in the corner. Nothing raises and `exitCode` is `0`, so this only ever shows up in
|
|
377
|
+
the delivered deck. Wrap every position and size in `Inches(...)` / `Pt(...)` /
|
|
378
|
+
`Emu(...)` and rerun — do not tune the raw numbers.
|
|
379
|
+
- On an `AttributeError` from python-pptx the API name is wrong, and on a `TypeError`
|
|
380
|
+
about missing positional arguments a required argument was left out — fix either
|
|
381
|
+
against this file's examples. Do not retry the same call, and do not switch to a shell.
|
|
382
|
+
- `AttributeError: 'Slide' object has no attribute 'add_picture'` (or `addpicture`,
|
|
383
|
+
`add_textbox`, `addtextbox`) means the call skipped `.shapes` — use
|
|
384
|
+
`slide.shapes.add_picture(...)` / `slide.shapes.add_textbox(...)`, never
|
|
385
|
+
`slide.add_*`.
|
|
386
|
+
- `AttributeError: 'SlideShapes' object has no attribute 'background'` means the
|
|
387
|
+
background was reached through the shapes collection — it lives on the slide:
|
|
388
|
+
`slide.background.fill.solid()` then `fill.fore_color.rgb = RGBColor(...)`.
|
|
389
|
+
- `AttributeError: '_Paragraph' object has no attribute 'fill'` means a fill was
|
|
390
|
+
asked of text — paragraphs have none. Color text with
|
|
391
|
+
`paragraph.font.color.rgb = RGBColor(...)`; `.fill` belongs to a shape or to
|
|
392
|
+
`slide.background`.
|
|
393
|
+
|
|
394
|
+
## Finish
|
|
395
|
+
|
|
396
|
+
When `exitCode` is `0` and `attachments` lists the `.pptx`, stop tool use and
|
|
397
|
+
answer with one line: file name + the slide count from stdout. Exactly one
|
|
398
|
+
successful build `exec` per request — re-running the same build is spam, not
|
|
399
|
+
quality.
|