Skip to main content

ppt-script-authoring

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.

Source facts

Repository
EverMind-AI/Raven
Last source activity
September 19, 2026 at 12:23
Detected SKILL.md language
English
Stars
5,465
Forks
170

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
13 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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 |
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub