- name
- ppt-script-authoring
- description
- Design and build a deck as a python-pptx program: your own layout system, the source's own figures, charts you draw, and a template's design when the user gave one.
- metadata
- {"raven":{"emoji":"🎞️","always":true}}
# Deck authoring
You design the deck and you build it, by writing a python-pptx program that
`ppt_build` runs. Every page is yours: the grid, the type scale, the palette, the page
furniture, where a figure sits and how large. What refuses a deck and what only reports
is in §12.
## 1. Define the communication job
Infer the audience, the deck's job, the outcome you want, the central takeaway and
the evidence that supports it. Build a cumulative argument rather than an inventory
of topics: each content page performs one narrative job and makes one primary claim.
Write audience-facing titles that state the point. Open with the context, question
or stakes that make the deck worth sitting through. Close by resolving that opening
— a decision, an implication, a next question — on the last content page. The
template's closing page ends the deck after it, as its cover opens it: both are the
house frame and carry no argument of their own.
Pack the evidence before allocating pages. A page carries one claim and the developed
support that claim takes, and what makes the support developed is that it is more than
one kind of thing: evidence from the source, a mechanism or a comparison, an
implication or a constraint. Nothing counts the kinds, and how much copy a page ends up
with is measured rather than allocated (§2). If the evidence cannot support a complete
argument, merge the page rather than enlarging fragments.
The language, the audience and the length are the user's, and they are asked for
rather than inferred. `ppt_prepare` reads what the request already states and hands
back the rest as questions under `ask_user`. **Put them to the user with the `ask_user`
tool, all of them in one call, each with the option you would recommend marked.** The
answer comes back through the host: in a direct chat the user answers; in a delegated
run the host answers what the delegating conversation already settled and puts the rest
to the user. An answer that is empty, or that says the question was not sent, means
nobody is attached to this run: then take the option you marked recommended -- the
request's own scope, the materials' language, the length its kind of deck usually has --
and record it with `ppt_brief` as the assumption it is, so the deck says what it assumed.
Ask once: a question nobody answered is not answered by asking it again, and a run that
asks a question a turn spends a turn on each.
The length and the language are then measured against the
finished file and both refuse the deck (§12), so record what the user said rather than
what you inferred. The audience is not measured; it is the room you judge the deck for
when you look at the renders (§10).
The fourth thing to record is the one that gets dropped: whatever the user ruled out. `ppt_brief(forbidden=["no icons", "never name a competitor"])` is
quoted back to you in so many words on **every** build, which is what keeps a
prohibition agreed at turn three alive at turn forty; the same thing filed under
`notes` is kept with the brief and never restated. Only what the user actually ruled
out belongs there — a preference you inferred, recorded as a rule, is a constraint
nobody agreed to.
## 2. Write the outline before you write the program
**Go looking for the deck's pictures first, and look widely.** Not for a page but for
the pool the outline gets to choose from. `ppt_image_search` takes every query at once
-- `queries: ["...", "..."]`, up to twelve, searched together and grouped by query in the
reply -- so the whole pool is one call, not one call per picture. It returns each
candidate with its pixel size and the page it came from. Search the things the material
*names* as well as the things it links — a paper, a benchmark, a product, a release —
and the ordinary furniture too, the logo and the product shot, which every deck wants and
no material bothers to link. Bring in more than one page will use: `ppt_fetch` each, and
`ppt_figure_inspect` them, because a candidate you have not looked at is a candidate you
cannot choose.
`ppt_outline` records it, and the build refuses until it exists. This is the stage that
decides how much is on a page: without one, what a page says gets decided while its
geometry is being typed, and the page comes out thin.
Per page: the **claim** as a statement (which is also its title), what **carries**
it, the **figures** it places by id, and what it **says** in the deck's language, as
it will read on the page.
The outline does not decide a page's *shape*. It used to record a table's columns and
cells for any page whose `carries` mentioned a comparison, and a brief that asks for a
「对照表」 is ordinary — so a deck came back with five tables whose every cell was a
phrase, planned before anyone had seen the content laid out. What a page carries is
settled where it is drawn, with
[deck/build/references/tables.md](deck/build/references/tables.md) open. **Figures a
reader compares down a column are a table** — a benchmark, a price list, a
year-by-year rate — and that page should draw one. Words in every cell are cards.
`says` is where density is decided, and it is the field that gets under-written. A page
carries 200 to 467 characters of copy and the **median is 250** — a
five-row comparison, two takeaways and a caveat is that page, and it is a good page.
Plan for the median rather than the top of the range: the top of it is where the type
reaches its floor and `overset_copy` starts reporting. Points that are each half a line
add up to a page that is a title and some labels, and the build cannot rescue it:
geometry can arrange copy, never supply it. So write each point as the sentence a reader
gets, evidence included, and if a page has only one short line in it either the evidence
for it belongs there too or the page belongs merged into its neighbour.
Checking the plan here rather than after the build is the difference between a cheap
edit and an expensive one: what refuses an outline, and what it only reports, is in §12.
And `needs` is where gathering belongs — this is the first moment anything knows
what each page will show. Go through the outline page by page and name the picture
each one wants, then find them all in one `ppt_image_search` (`queries: [...]`, one per
page that wants a picture) and bring in what you pick with one `ppt_fetch`. A fetch joins the deck's own source set and is read on arrival, so there
is no separate ingest to remember; `ppt_ingest` is for bringing in another directory
of the user's, or re-reading after you have edited a source by hand.
A deck whose only imagery is what the materials happened to contain is a deck of
panels: the products it names have logos, the organisations have marks, the standards
have badges, and the architectures have diagrams their own documentation draws. Search
for those before deciding a page is text.
**What a page is missing is searched for, not filled in.** The two paths above gather
pictures; the same move answers a claim the sources leave without its evidence.
`web_search` for it, `web_fetch` the page that carries it, and keep where it came from
beside what you took. Nothing on a page is invented: not a number, not a source, not a
person, not a place. A gap you went looking for and did not find is worth keeping -- say
in `needs` what is missing and who has to supply it -- and a gap nobody looked for is a
page handing its own work back to the reader.
## 3. One visual identity, defined once
**What a finished page owes is written down, and it is what the review at the end judges
by: [deck/build/references/design-requirements.md](deck/build/references/design-requirements.md).
Open it now, before the first page.** Seven short sections -- room, edges, marks, figures,
tables, type, the claim -- each decidable from the render alone. Two of them are here as well:
- **A blank region has two kinds and they have different answers.** A shape bigger than
what it holds is fixed by measuring (`card_size`, `text_size`, `picture_size`,
`table_size`) and taking only that much. A *page* bigger than what it holds is not
fixed by shrinking anything -- taking a cover off room leaves the room -- and wants
either more that the page needs said, or this page and its neighbour becoming one page.
The test is one question: would making the shape smaller fill the page?
- **A table spans the box you hand it**, in the proportions its columns measure, and
spreads its rows into it down to five rows or so. So the box is the whole answer: give
a page-wide table `frame.body` and a two-column one a `split_left` column, never the
body with `weights` guessed to make up the difference.
Take the palette and font from `ppt_theme`. It is written into the build directory on
every build, so the import is plain — that directory is already on `sys.path`:
```python
from ppt_theme import THEMES, rgb
theme_id = next(iter(THEMES)) # one entry, and it is the template's
T = THEMES[theme_id]
BG, SURFACE = rgb(T["background"]), rgb(T["surface"])
INK, MUTED, ACCENT = rgb(T["foreground"]), rgb(T["muted"]), rgb(T["accent"])
SOFT, GRID, ACCENT_INK = rgb(T["accent_soft"]), rgb(T["grid"]), rgb(T["accent_ink"])
SERIES = [rgb(c) for c in T["chart_series"]] # six, in order
FONT = T["font_family"]
```
**Do not write a module into that directory whose name shadows a standard one.** A
`copy.py` beside your script breaks python-pptx's own import, and the traceback will
not mention your file.
**There is no palette to pick, and one to state.** A deck is always built inside a
template: `ppt_prepare` binds the one the user gave, and binds a bundled default when
the user gave none. The build directory then holds one theme, named after that file —
so `THEMES` has exactly one entry and its face is the template's own. Take that entry
by iteration; a theme id typed into `THEMES[...]` is a `KeyError`. Where the renders
show a ground or an accent the file does not declare, state what you see once with
`ppt_template(palette=...)` before the build, and every page and every derived tint
follow from it; a `T["background"] = ...` in the program changes one run's pages and
records nothing.
**Only three roles are read off the file** -- the ground, the ink and its first accent.
`surface`, `accent_soft`, `accent_ink`, `grid` and `muted` are mixed from those three.
What a file declares is not what its pages paint -- all ten bundled templates declare
`#F0F0F0` as their second light colour and none of them paints it -- so say what the
renders show:
`ppt_template(project=..., palette={"accent": "#155FFD", "surface": "#DDE8FF"})` holds
for the deck's whole life and reaches every page through `ppt_theme`.
**A partial palette is the ordinary one.** What you leave out is re-derived from what
you named: `palette={"accent": "#7A1FA2"}` in an amber template brings `surface`,
`accent_soft` and `accent_ink` back as tints of that purple. Reach for it for a deck
with a brand of its own, an accent the template does not carry, or a series the content
decides -- `chart_series` is the one role a stated accent does *not* move, so a
comparison drawn in your own two colours states them. A later call merges over the
earlier one, so state it before the first page.
The theme part is what the build checks, and you pass it by opening `PPT_TEMPLATE` at
all (§12). Nothing checks what colour you filled a shape with.
**The role names are open; a role's meaning is not.** These eight are the ones the
derivation knows how to finish:
| Role | What it is for |
| --- | --- |
| `background` | the ground a page is painted on |
| `surface`, `accent_soft` | planes content sits on |
| `foreground` | body type |
| `muted` | secondary type and source lines |
| `accent` | the focal point of a page |
| `accent_ink` | type in the accent's colour (below) |
| `grid` | hairlines |
You may add your own beside them: a deck comparing two things wants two colours for the
pair, and `palette={"ours": "#0B3D91", "theirs": "#F2E8D5"}` puts both in every page's
theme, where `card(..., tint="ours")` reaches them by name. What may not move is the
meaning — if the accent marks "our result" on one page it cannot mark "prior work" on
the next, and a pair named on page 3 is the same pair on page 11.
**A helper that holds the theme takes a colour by name.** `tint=` and
`colour=`/`mark_colour=` -- the signatures in §4 say which helper takes which -- each
take a role name, a key you added to the palette or to your own copy of the theme dict,
or a literal `#RRGGBB`. A name the theme does not carry is refused and says so, so a
misspelling is a message and not a colour.
`write` is the exception and the reason is in its signature: it is handed a box and a
string and no theme, so it takes the colour itself — `colour=theme["muted"]`,
`colour=theme["accent_ink"]`, or hex. `"muted"` there is six characters of a hex digit
and raises as one.
**A chart's `accent=` is not a colour.** It names *which item* to bring forward — a
category from the data you passed, `accent="Q3"` — and handing it `#RRGGBB` is refused
with the list of items it accepts.
A chart's colours are `chart_series`, and that is yours to set. Its order is the
contract — `chart_series[0]` is the series the page is about — so a comparison reads
by putting your own two colours in it: for the deck, `ppt_template(palette=
{"chart_series": ["#0B3D91", "#E4572E"]})`; for one page, copy the theme and set it
there before the call. A chart drawn from a theme you did not touch uses the
template's own series, which is the right default and not a rule.
**A card chooses its own ink.** `card` and `heading` set their type in whichever of the
theme's inks reads on the plane they were given, so `card(..., tint="accent")` on a
deep blue comes out with white type and the same call on a pale tint comes out dark.
You do not compute that, and you should not work around it: a `write` you place over a
plane yourself is yours to colour, and `theme["background"]` is the ink on a saturated
one.
**`accent` fills; `accent_ink` writes — on the page's own ground.** `accent_soft` is
the accent mixed towards the background, so a pale accent cannot be read on its own
tint. The theme carries `accent_ink` for this: the same hue, dark enough to write with
on the ground and on `accent_soft`, or the accent itself where that already reads.
Fills, markers and bars take `accent`; a number or heading in the accent's colour takes
`accent_ink`, by that name or as `theme["accent_ink"]`. On a plane of `accent` or a
page whose ground is saturated, `accent_ink` is a dark on a dark — a deep red heading
on a red page measured 1.09:1 — so type there is `theme["background"]`, the light ink.
**A paragraph set in one weight and one grey says nothing is more important than
anything else.** A reader scanning a slide takes the emphasised words first and reads
the rest only if those earned it, so every body paragraph that carries a number, a name
or a verdict marks that part and leaves the rest alone. What gets marked is the load:
the quantity and its unit, the one term the sentence turns on, the word that makes a
comparison a comparison. What does not is a phrase the author found interesting, and a
paragraph with four marked words has marked none.
Two marks and no third. **`accent_ink`, for the thing the page is about** -- one per
paragraph, at most two or three on a page, and if everything is in the accent the accent
has stopped meaning anything. **Bold, for a term the reader has to hold on to** -- a
label, a defined name, the head of a list item. Not italics, not underline, not a third
colour: a deck reads as one document when its emphasis has one vocabulary. A number that
carries the whole page is not emphasis at all -- it is the page's anchor, and it goes up
the size ladder as well (`no_anchor`).
`Run` is how a line says it, and §4 has the call. Where the whole block is the
emphasis -- a lead line, a pull quote, a card's heading -- `write`'s own `bold` and
`colour` are simpler and say the same thing.
**One font family, and its CJK companion.** Inside a template, the face `ppt_theme`
hands you is the template's own, whatever it is (微软雅黑, say), and that is the right
one to use: it is the house style, and the width measurement degrades to an estimate
for it rather than failing. Anything else is substituted on export and every position
you computed is wrong.
For a deck in Chinese that is only half the answer. `font_family` is a Latin face,
and Han characters set in one fall back to whatever the viewer has — a different
design at a different weight, and on the review renderer no Han glyphs at all. Every
theme carries `cjk_font_family` beside it — the template's own Han face where it names
one, and otherwise a face matched to the Latin one's class (`Noto Serif CJK SC` for a
serif, `Noto Sans CJK SC` for a sans) — and both names go on
the same run: `write(..., font=FONT, cjk_font=HAN)` from `ppt_layout` does it, or
set `a:ea` yourself. Then 目标查询 is set on purpose and `TarViS` in the same line
stays in the Latin face.
**A type scale with a big step at the top and fine steps below.** Same size for the
same role on every page. A 16.5pt title over 16pt body is not a scale; it is no
hierarchy at all. Two numbers in it are measured rather than chosen, because a deck is
read projected: body copy stays at or above **14pt**, and a caption, a source line or
any other short label may go to **10.8pt** but no further.
**A dominant element takes the top of the scale, and the top of it is above the title.**
A page whose point is one number sets that number at `NUMBER_PT`, the ramp's largest step
-- `the_largest_step_this_copy_takes(..., largest=NUMBER_PT)` is how to ask for it (§4) --
and it outranks the page title rather than sitting under it: a number set smaller than
the heading over it is not a dominant number, it is a caption on one. The inversion runs
the other way too, because copy is copy -- a concluding sentence set larger than its own
title makes the title the subordinate line. Where the size a page seems to want falls
between two steps, take the step above rather than a size off the ramp (§12).
Which floor a box answers to is decided from the box, not from you:
| The box | Floor |
| --- | --- |
| in the bottom 0.9in of the page -- furniture nobody reads from a seat | **8pt** |
| twenty characters or more, above that band -- copy | **14pt** |
| opening with `来源`, `注:`, `图`, `表`, `source:`, `figure` or `table` -- a caption in either language | **10.8** |
| shorter than twenty characters -- a label | **10.8** |
| fewer than four characters -- a page number or a chart tick | not measured |
The size measured is the one the *render* came out at, not the one you declared, so a
box too small for its copy is caught where autofit shrank it. Copy that will not fit
above the floor is a page with too much on it, and §12 says what to do instead of
shrinking it.
**One set of spacings, one surface treatment, one rule weight.** Fix them in the
setup beside the palette and call them everywhere: a larger gap between groups and a
smaller one inside a group, so grouping is legible from the spacing alone; one
restrained tint that planes are painted with; one thickness a hairline is drawn at.
Spacing that changes page to page reads as a deck assembled rather than designed, and
two groups with no more air between them than inside them come back as
`unseparated_blocks`.
**The header is the same on every page.** It is the one thing a reader sees eighteen
times, so the deck picks one header form and keeps it: the same kicker, the same
title position, the same treatment. Which form is yours, within the one the template
already has: its own title row, measured for you (§8). What a deck may not have is
three of them.
**Avoid -- the treatments that read as generated.** Each is a default reached for
instead of a decision, and each is legible as one from the back of the room:
- **A decorative rule or accent line under a title.** The title's own step in the scale
already separates it from the page; a line drawn under it adds a horizon that cuts the
heading off what it heads, and it appears on every page because it was never chosen
for any of them. `heading` draws none, and `rule` caps at 1.05in so it cannot become
one -- it is a short mark under a heading or beside a number. A page that genuinely
wants a divider gets a hairline under 4.5pt, or a thin `plane` for a full-width one.
- **A filled shape whose content is the space it covers.** A colour bar across the page,
a saturated strip down the edge of a card, a tinted panel painted over whatever room
was left with one sentence centred in it: each is a shape carrying no information,
which is what makes it read as filler -- a reader sees a region announced and finds
nothing in it, and the emptiness the shape was covering is now emphasised instead.
Grow the load-bearing content or redistribute it (§3.5). One filled bar is the deck's
own, the band the title row sits on; `band` (§12) says which shapes and at what size,
and `excessive_whitespace` measures what the panel was hiding.
The call is `card_size`, and a region is not a height: `card_size(w, icon=, title=,
body=)` answers how tall one card's own content makes it, so a row of them is
`max(card_size(...).h for each)` and the row is drawn at that. Take only that much of
the region and leave the rest to the next band -- a card handed `cells[i]` straight
out of `grid` is as tall as the region however little it holds, which is the void.
Cards take an `icon` for the same reason: three lines under a mark fill a card that
three lines alone leave two thirds empty, and the mark is what makes the row scannable
rather than three paragraphs side by side.
**The bar is usually not a mistake -- it is an answer to a page that felt empty**, and
wanting a full page is right. A stretched shape is the wrong way to get one, because it
fills the pixels and not the page: the reader still finds three short lines in a
half-page region, only now with a colour announcing it. What fills a page is content,
and the moves are these, in this order -- say more on the page (a line of detail under
each card title, the figure the number came from, the units, the year); give each card
an `icon`; use fewer, wider cards so the copy reaches their edges; put the figure or
the table in the room the cards left; or move a point here from a page that has one too
many. Only when none of those is true is the page genuinely short, and then the answer
is one page fewer, not one bar more. If you have already drawn the bar, the check is
the same either way: cover the shape's fill with your hand and ask whether the page
still says as much. If it does, the fill was doing nothing.
- **A title too close in size to its body copy.** The step at the top of the ramp is
what makes a title a title, and two sizes a point apart are one size to a reader.
`type_scale` and `type_drift` (§12) read the sizes back off the render.
- **Type too small to read projected.** The floors above are the measured half of this;
the other half is the page shrunk to a third of its size (§10).
- **The same page skeleton with only the words changed.** The header repeats on purpose
and nothing under it does: a cover, an agenda, a two-way comparison, a table page, a
full-bleed figure and a closing page are each recognisably a different kind of page.
`layout_variety` (§12) reads this off the shapes each page actually carries.
- **A filled chip behind every icon.** A row of identical coloured badges is the tell.
An icon is a mark beside type (§7); where a block wants a surface, give the whole
block one.
## 3.5 Structure follows the content
Decide what carries the point before placing anything.
**Every content page carries a visual element** -- a source figure, a chart you drew, a
drawn table, a diagram, or a region led by icons. A title over a column of copy is the
page that comes out when nothing was decided. The deck-wide count, its threshold and what
counts are `evidence`'s (§12); the decision here is which element this page's claim is
made of.
**Reference — not a constraint.** The table below is vocabulary, not a lookup. A page
may take one row, several rows together, or a composition no row names; no row carries
a coverage quota and nothing counts which ones a deck used.
| Content logic | What carries it |
Auf GitHub ansehen