Agent Skills › EverMind-AI/Raven › ppt-script-authoring

ppt-script-authoring

GitHub

通过编写Python代码自动生成PPT演示文稿,支持自定义布局、图表及模板设计。涵盖受众分析、内容构建及交互式需求确认流程。

plugins-dist/ppt-engine/raven_ppt/skill/ppt-script-authoring/SKILL.md EverMind-AI/Raven

Trigger Scenarios

需要生成或修改PPT文件 使用python-pptx进行自动化幻灯片创建

Install

npx skills add EverMind-AI/Raven --skill ppt-script-authoring -g -y
More Options

Non-standard path

npx skills add https://github.com/EverMind-AI/Raven/tree/main/plugins-dist/ppt-engine/raven_ppt/skill/ppt-script-authoring -g -y

Use without installing

npx skills use EverMind-AI/Raven@ppt-script-authoring

指定 Agent (Claude Code)

npx skills add EverMind-AI/Raven --skill ppt-script-authoring -a claude-code -g -y

安装 repo 全部 skill

npx skills add EverMind-AI/Raven --all -g -y

预览 repo 内 skill

npx skills add EverMind-AI/Raven --list

SKILL.md

Frontmatter
{
    "name": "ppt-script-authoring",
    "metadata": {
        "raven": {
            "emoji": "🎞️",
            "always": true
        }
    },
    "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."
}

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 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. 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:

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
Real product, interface, experiment or published plot a source figure
Source-backed numeric comparison or trend a chart you draw
Tabular data or comparable rows and columns -- figures a reader compares down a column a drawn table or comparison matrix; not for four labelled points, which are cards or a list
Familiar capabilities or categories icon-led regions
Sequential steps or milestones a timeline, a numbered column, or stages as unequal blocks; §7.5
Two genuinely contrasted alternatives paired regions on one baseline
One memorable conclusion a single dominant statement
One dominant number the number at display size, reasoning beside it

Then compose around it. Give the load-bearing element the space its role deserves and let the rest defer; a page where every region carries equal weight has argued nothing. Vary composition because the content varies, not to fill a quota, and keep a series of comparable cases on one shape so the reader can compare them. Give a conclusion or decision its own baseline, weight or restrained accent instead of styling every component identically.

Prefer one coherent composition over a dashboard of unrelated panels. Do not draw boxes merely to avoid a prose page.

Three blocks of copy in one body need something drawn between them. Two read as a pair from their positions alone -- a claim and its qualifier, a before and an after. Three stop doing that, and their grouping becomes a grid the reader has to infer from x-offsets. One device is the whole floor and any of them counts: card_group for blocks that are the same kind of thing, plane behind the one region that answers the page, rule between two halves that are not cards, or a figure the copy is set against. A plane under a picture takes the page's own ground -- on a dark page a dark surface, never a pale one -- and a picture that carries its own ground goes on the page without one. Measured over the 105 content pages the bundled templates ship: 104 draw at least one, the median page draws seven, and every page that draws exactly one draws a picture. undivided_body (§12) reports the page that draws none. This is not the previous paragraph's box-for-its-own-sake -- it is the difference between three groups a reader can see and three paragraphs a reader has to sort.

With a template bound, a page takes its shape from an example page first (§8): the outline names the example each content page starts from, and composing is for the page no example or borrowable page can carry. A composed page takes its shape from the catalogue. Open deck/build/references/layouts.md while deciding. Its Part 1 is eleven skeletons -- every id folded into the one it varies, with the share of the body each division measured on the pages that were drawn -- and the outline names what you chose: layout takes the one structure id, layers the modifiers stacked on it, and anti_pattern the way this page would go wrong. A page cloned from a template example leaves layout empty, because its structure is the example's.

Every band of the page is declared, and the hierarchy is legible. A composed page divides into regions before it divides into shapes: count them, and give each the layer that says what it is (layers), because the band nobody planned is the one that comes back as bare paragraphs. Then rank them so the reader sees it before reading a word -- which region is the claim, which supports it, which is aside.

Cards, planes and helper presets are primitives, not a required page grammar. Do not wrap every component in a filled rectangle just to make its boundary visible; use hierarchy, alignment, whitespace and meaningful rules first.

Fill the page. A 13.3 x 7.5in canvas holds far more than a title and four bullets, and content that stops two thirds of the way down is the most common failure there is. Whitespace you chose is a margin around content that fills the frame; whitespace left over reads as unfinished. When a page comes back with a large accidental empty field, grow the load-bearing content, introduce a second visual layer, or redistribute what is already there -- never decoration, restated copy or smaller type, each of which makes the page worse than the gap did.

When the source already carries a real table, comparable rows or a set of parallel items with several attributes, that information shape is part of the evidence: do not flatten it because the text happens to fit.

On a figure-and-text page, let the figure dominate and turn the supporting copy into scan points with clear labels -- as many as the region beside the figure actually holds, which points_size answers before the page is drawn rather than after (§4). Put those points on one or more restrained theme-coloured surfaces so the image and explanation read as distinct layers; do not leave a bare paragraph floating beside the figure. The cards defer to the figure -- they are supporting structure, not a row of equal dashboard tiles.

The caption goes in the page's foot, not under the figure. A line of small type under every picture puts a second row of furniture in the middle of the body, and on a page with two figures it puts two -- the reader's eye leaves the figure, reads a label, and comes back. Collect what the figures need into footer()'s note, joined with ;: footer(slide, frame.footer, T, note="来源:xxx;图 1 主街三段式动线"). Where the note will not fit, keep the source and the one caption that carries a fact, and drop the rest -- never shrink the type to fit it in; footer measures the note and refuses a second line rather than drawing it into the margin. A number or a label on the figure is not a caption and stays where it is, and so is the label that says which cell of a small multiple this is: five identically framed figures are compared by their labels, and moving those to the foot leaves the reader counting cells.

It is measured, not only a preference. A caption strip under a figure at the foot of the body puts its own top edge inside the band footer_findings reads as the page's foot -- on five reference pages, three of them at 6.40in and 6.44in. Three fifths is over the share that reading takes as a habit, so the deck was judged to have feet, and then reported as unnumbered_pages because a caption carries no slidenum. A caption under a figure manufactures a false foot and a false warning at once.

Asking for the foot costs more than the caption frees. page(footer=True) takes _FOOTER_H + GUTTER = 0.58in off the body -- 5.25in becomes 4.67in -- and a caption strip is about 0.40in. So a page that gains a foot has a smaller body than before, by about 0.18in, and its bands have to be measured again: two reference passages were laid out against 5.25in and take refused them at the band where the room ran out. Re-measure with short_by before drawing, and where a band has to go, cut a heading whose cards already name themselves rather than the content.

Every placed figure is still accounted for, and a figure can carry two claims that are not the same kind. Its source caption is what the source printed under it: quote it, credit it. The visual_caption ppt_figure_inspect wrote is a description of the pixels and nobody's caption: write your own line from it, never present it as the source's words, and never put a name in front of it the materials do not establish -- a figure credited to the product it happens to sit beside is the failure this exists to stop. A caption says what is shown, not a claim the pixels do not prove.

4. The program

It goes at deck/build/build.py — that whole path, relative to the workspace; ppt_prepare reports it as write_the_program_to. A bare build/build.py lands somewhere the build does not look. write_file creates it, write_file with mode="append" extends it, edit_file revises it. Never restate the whole file to change part of it.

Write it in pieces, not in one call -- and in few pieces. write_file for the setup and the first five pages, then mode="append" for five pages at a time, with ppt_build(draft=true) in the same reply as each append -- the calls in one reply run in the order you list them, so the write and the build together are one turn: a draft builds what exists, measures it and hands back the renders without holding a part-written deck to the agreed length and without publishing. Four appends and four drafts is a twenty-page deck. Use write_file, not exec, to write the program. Drop draft when the deck is whole and you want the gates. A twenty-page program sent in one call is cut before it arrives, and a draft shows you a page while there are three rather than twenty.

It runs in the build directory with python-pptx and Pillow and reads its paths from the environment: PPT_OUTPUT (save there and nowhere else); PPT_FIGURES_DIR (the figures ingest extracted, and the only way to one — take FIGURES = os.environ["PPT_FIGURES_DIR"] in the setup and write f"{FIGURES}/fig2.png" wherever a page places a figure, with the name the figure list gives after file — the id before it is the catalogue's key, not a file; there is no figures directory under deck/build); and, with a template bound, two paths that are not interchangeable — PPT_TEMPLATE is the template with its example pages removed, the deck you build into (prs = Presentation(os.environ['PPT_TEMPLATE'])), and PPT_TEMPLATE_SOURCE is the user's original with those pages still in it, the only thing prototype can read a page out of (tpl = Presentation(os.environ['PPT_TEMPLATE_SOURCE'])). Handing prototype the deck you build into raises this template ships 0 pages. PPT_BUNDLED_TEMPLATES is the folder the bundled templates ship in; bundled(name) reads it, so a page borrowed from another template (§8) needs no path in the program. PPT_SLIDE_BLOCKS and PPT_SLIDE_LINES are the build's own bookkeeping -- the # SLIDE n spans it runs one at a time (§4) and the record of which line drew which page -- and nothing in the program reads them.

One block per page, opened with a # SLIDE <n> banner. Shared helpers above the blocks; the page's own composition inside the page's own block. Two shapes are refused: a build.py that runs another file, and a loop that draws every page from one call site. Both leave no code belonging to one page, so nothing can match a render to the code that drew it, report a defect against the block that caused it, or fix one page without touching another.

The whole program, ending included. Every other example here and in the references is a page fragment; this is the file they go inside, and its last line is the one that gets left out. A program that draws twenty pages and never saves them leaves no deck.

import os

from pptx import Presentation

from ppt_layout import BODY_PT, heading, page, points, write
from ppt_theme import THEMES, rgb

prs = Presentation(os.environ["PPT_TEMPLATE"])       # the template, its example pages gone
T = THEMES[next(iter(THEMES))]                       # one entry, and it is the template's
FACE, HAN = T["font_family"], T["cjk_font_family"]
INK = rgb(T["foreground"])
HOUSE = "Title Only"                                 # house_style's layout_for_a_page_you_draw
LAYOUT = next(one for one in prs.slide_layouts if one.name == HOUSE)


def content_page():                                  # the one skeleton every page shares
    return prs.slides.add_slide(LAYOUT), page()      # the template's background comes with it


# SLIDE 1
slide, frame = content_page()
heading(slide, frame, T, "四类任务,一套权重", "01 结论", font=FACE, cjk_font=HAN)
write(slide, frame.body, "切任务只换输入查询,不重训。", size=BODY_PT, colour=INK,
      font=FACE, cjk_font=HAN)

# SLIDE 2
slide, frame = content_page()
heading(slide, frame, T, "证据", "02 消融", font=FACE, cjk_font=HAN)
points(slide, frame.body, T, ["去掉时序颈:44.7 对 46.3", "去掉语义查询:不收敛"],
       font=FACE, cjk_font=HAN)

prs.save(os.environ["PPT_OUTPUT"])                   # the last line of every program

prs.save(os.environ["PPT_OUTPUT"]) is the last line of every program, draft builds included. Nothing saves for you and nothing else is the place to save to.

Then write the helpers your pages need. Beyond the header, the source line and the page number, page types have no reason to share a skeleton (§3).

Default — a * after the geometry in every helper you write. def card(slide, x, y, w, h, *, fill=SOFT, bold=False, align="left") — the boxes stay positional because they are always the same four numbers in the same order, and everything after them is named at the call site. Seven or eight positional parameters is the largest single cause of a build that will not run at all: an empty string where a bold flag goes, an argument short, a keyword the function never had. ppt_layout's own helpers are shaped this way.

The agenda is the page that goes wrong most often. It is a map of the argument: the movements the talk makes, each named with one line saying what it settles. Default — five to seven of them, and nothing counts them; the outline's own sections usually run to eight to twelve, so a deck may well want more. What is not a default: it is not an index of slide numbers. "3 Introduction, 4 Limitations, 5 Overview…" enumerates the file rather than the talk, and tells an audience nothing the page numbers do not.

Ask how wide the text is instead of guessing at it. The commonest geometry bug in a generated deck: a box sized to what the string looks like wraps it onto a second line, the second line pushes into whatever sits below, and the page reads as broken rather than as tight. text_size is the width and height the copy really takes and fits is the yes-or-no, both answered before anything is placed. Where a line must not break whatever happens -- a number, a label, a kicker, a card heading -- say so on the frame write hands back: write(...).shape.word_wrap = False, where .shape is the text frame itself. Extra width costs nothing; an unfilled text box has no fill and no outline.

print() is how those answers reach you. Whatever the program writes to stdout comes back in the build reply, on a failed build as well as one that worked -- so text_size, fits, formula_type_size, len(units(slide)) and the theme your script actually resolved are one print from being visible. Print the measurement, not the whole theme: the last 20,000 characters are what comes back. It is also the only way to find a page that runs and draws the wrong thing.

The agenda's geometry breaks the same three ways every time, so compute it:

  • A two-digit number needs a box that fits two digits. 01 and 10 fold into two stacked characters in a box sized for 1. Set write(...).shape.word_wrap = False, or give the box the width the widest label needs.
  • Lay repeated rows on a pitch. With n rows in height H the pitch is H / n, the label sits at y = top + i * pitch, its description at a fixed offset below.
  • A label and its description are one block. If the description can run to two lines, the pitch has to allow for two, or row i + 1 lands on top of it.

The same three apply to any page built from repeated rows or cards.

The helpers, by signature

Everything below is already beside your script: what each helper takes, what it hands back, and what it can be asked before it draws. Read this rather than the modules (51k tokens of source, in context for the rest of the run), and a helper that is neither here nor in a reference this page links is one you should not be calling. The drawing base ppt_charts is built on is left out below and is in §6.

Import them by name. What follows is the whole set this page and its references call, so a snippet copied from either has its import here; take the lines the page you are drawing needs, and an import you never call costs nothing. A name used in an example and absent from every import line is a NameError at build time, so the import travels with the snippet.

from ppt_theme import THEMES, rgb
from ppt_layout import (
    BODY_PT, Box, CANVAS_H, CANVAS_W, Drawn, Frame, GUTTER, Inches, KICKER_PT, LABEL_PT,
    LEAD_PT, MARGIN, MSO_SHAPE, NUMBER_PT, PAD, Pt, Run, TITLE_PT, card, card_body_box,
    card_size, fits, footer, formula, heading, mark, page, picture_fit, picture_size, plane,
    points, rule, stack, table, table_size, text_size, the_largest_step_this_copy_takes,
    what_this_page_gave_up, write
)
from ppt_shapes import connect, preset, timeline
from ppt_icons import add_icon, find_icons, the_ink_an_icon_covers
from ppt_charts import (
    TooSmall, column, contrast, fmt, hline, horizontal_bar, ink_on, key, line_height, linear,
    number, pick_size, poly, rect, shades, span, text_width, type_face, vline, waterfall,
    write_label
)
from ppt_template import (
    add_unit, backdrop, bundled, clear_region, clone_page, clone_shape, drop_shape,
    layout_pictures, page_box, prototype, raise_type, remove_unit, replace_picture, replace_text,
    shape_at, shape_near, shape_saying, shapes_in, units, wash
)

Every deck/build/references/... link on this page is a file in the build directory, beside the modules it documents, and that whole path is what read_file takes -- the file tools resolve against the workspace, not the directory your program runs in. A table drawn without reading its reference gets the bare default.

ppt_layout, always present:

page(kicker=True, footer=False) → Frame(kicker, title, body, footer), four boxes inside the safe area; ask for the footer on a page that cites or carries a page number, and the body gives up the strip for it
footer(slide, box, theme, *, note=None, number=True, colour=None, font=None, cjk_font=None) draws that strip: a hairline across it, note on the left, and the page number on the right as a real slidenum field. note is one line -- the strip is the page's bottom edge and a second line goes into the margin, so a note too long for the lane (the strip less 1.6in for the number) comes back as this call refusing, with the width it had and the width it needed. Every bundled template defines a page-number placeholder on its master and puts one on no page, and python-pptx does not clone a footer placeholder onto a slide — so a deck gets page numbers only if the script draws them
Frame(kicker, title, body, footer) the same four boxes as a value you can build. page() is the ordinary page and not the only one: a frame you make yourself is what a left rail, a full-bleed opener or a title over two thirds of the canvas is made of, and every helper that takes a frame takes yours without knowing the difference
frame.laying(*heights, gutter=GUTTER) the default way to lay a content page's body. A cursor down the body with the leftover already spent between the bands, so the last band ends on the body's own floor. Pass the bands, take them back in the same order, and no skip between them -- the gaps are what the leftover became. Two bands or more; one band has no gap to spend and comes back centred, which is holding's case
frame.holding(*heights) the same frame with its body cut to the run these heights add up to, and the leftover split as air above and below that run instead of a band of white along the page's foot. The bands and the gaps between them, in the order they occur; a list works too. For the path where the page measures its bands and takes them off a cursor -- not for a run something else already spreads into the whole body (card_group(..., down=True)), whose cards would then touch. Only the body moves, so it can be asked before or after heading, and a run with no slack comes back unchanged
Box.corners(x0, y0, x1, y1) a box from its two corners
Box.at(x, y, w=, h=) a box from a corner and a size; the size is keyword-only. It reads back under the same four names (box.x, box.y, box.w, box.h) as well as box.x0..y1
box.rows(n, gutter=GUTTER, weights=None), box.columns(...) n boxes filling this one
box.grid(cols, rows, gutter=GUTTER) row-major cells
box.split_left(fraction, gutter=GUTTER), box.split_top(...) two boxes, the first taking fraction
box.inset(dx=PAD, dy=None) a smaller box inside this one
stack(box, gutter=0) a cursor down a region: .take(height), .rest(), .skip(height), .left, .short_by(*heights), .spread(*heights), .centre(*heights); bands are adjacent, skip is the gap
picture_fit(slide, image, box, theme, *, caption=None, size=LABEL_PT, align="center", font=None, cjk_font=None) a picture scaled to fit the box whole, centred. caption draws a strip under it and the deck's captions do not go there -- they go in footer(note=); it is for the label that says which cell of a small multiple this is
heading(slide, frame, theme, title, kicker=None, *, tint="surface", bleed=True, size=TITLE_PT, anchor="middle", font=None, cjk_font=None) §6.5 — anchor is the template's, not this default; §8
write(slide, box, text, *, size=BODY_PT, colour="#000000", font=None, cjk_font=None, bold=False, align="left", anchor="top", spacing=1.15) text may be a list of paragraphs
points(slide, box, theme, items, *, size=BODY_PT, numbered=False, mark="•", colour=None, font=None, cjk_font=None, mark_colour=None, spacing=1.25) §6.5 — it takes the theme, so the faces are optional here
card(slide, box, theme, *, icon=None, title="", body=(), tint="surface", size=BODY_PT, title_size=LEAD_PT, font=None, cjk_font=None) §7
card_group(slide, box, theme, items, *, down=False, gutter=GUTTER) a row of cards across the region, or a column down it with down=True. Each item is a dict of card's own arguments, so every field it carries reaches the card; a row is levelled with card_size and centred, a column keeps each card's own height and spreads the leftover. §7
formula(slide, box, text, theme, *, size=BODY_PT, align="left", anchor="top", font=None, cjk_font=None) §9
plane(slide, box, theme, tint="surface", radius=True, opacity=1.0) a painted region, rounded. A group of copy that sits on the page's own ground has no edge, and a reader cannot see where one group ends and the next begins -- three columns of body copy under a band came back as "the components have no boundaries". Put every group that is not page furniture on a plane; radius=False only for a band running to the trim
rule(slide, box, theme, thickness=0.03, colour=None) a hairline 0.06in below the box and at most 1.05in long — a short mark under a heading or beside a number, not a divider across a region. For a full-width line draw a thin plane, or take the box a chart hands back
table(slide, box, rows, theme, *, weights=None, size=LABEL_PT, numeric_from=None, style="header_tint", emphasize_rows=(), emphasize_columns=(), group_rows=None, indent_rows=(), total_rows=(), marks=None, header_size=None, align=None, rule_pt=None, grid_pt=None, row_height=None, header_height=None, padding=None, fill=True, column_rules=True, banding=False, fills=None) §6
Run(text, size=None, bold=None, colour=None) one stretch of a line with its own size, weight or colour — a paragraph handed to write may be a list of these instead of a string
mark(slide, box, theme, kind, value=None, *, colour=None) deck/build/references/tables.md
overlaps(boxes, tolerance=0.01) → the (i, j) pairs that overlap

Constants: CANVAS_W, CANVAS_H, MARGIN, GUTTER, PAD; the ramp TITLE_PT, LEAD_PT, BODY_PT, LABEL_PT, KICKER_PT, NUMBER_PT, BODY_FLOOR_PT.

What comes back, and how to ask before you draw

Every helper that draws hands back a Drawn — the five that compute instead hand back what they computed: page() a Frame, stack() a Stack, overlaps() a list of (i, j) pairs, a Box divider a Box or a list of them, and mark() a Marks, which is a list of the shapes it made carrying .box for the ink they actually cover — the one measurement that says whether a marked column is wide enough to read. A card_group reads the same way: the cards' own Drawns in order, carrying .box for the run as a whole.

For the drawing ones: .shape is the python-pptx object it made and .box is what it actually covered, not the box you passed in -- a rule sits below the box it underlines, a picture keeps its own aspect, and copy that did not fit comes back taller than its box. An attribute the tuple does not carry is looked for on the shape and then on the box, so write(...).paragraphs, table(...).columns and heading(...).x1 read straight through. Place the next thing on the page off the last thing's .box, never off the number you chose for it; overlaps takes those boxes and names the pairs that collide.

And every one of them can be asked before it draws, which is what turns a render round into an if:

lines_needed(text, width, *, size=BODY_PT, font=None, bold=False) how many lines this copy wraps onto at that width
text_size(text, width, *, size=BODY_PT, font=None, bold=False, spacing=1.15) the box the copy really needs, at the origin: .h is what to ask a stack for, .w is what the longest line actually sets
points_size(items, width, *, size=BODY_PT, font=None, spacing=1.25) the same for a bulleted list, whose hanging mark and paragraph spacing text_size knows nothing about
fits(what, box, *, size=BODY_PT, font=None, bold=False, spacing=1.15) yes or no. what is copy, or any box one of these handed back
table_size(rows, theme, *, weights=None, size=LABEL_PT, style="header_tint", numeric_from=None, group_rows=None, marks=None, header_size=None, indent_rows=(), row_height=None, header_height=None, padding=None, fill=True, box=None) where the table ends, before a cell of it is drawn. With box it is placed at that box's corner and held to its width
picture_size(image, box, *, caption=None, size=LABEL_PT) the room the figure needs inside box, off the image's own pixels. Pass caption only where picture_fit is going to draw one, or the reservation is for a strip nothing fills
formula_type_size(text, width, *, size=BODY_PT, font=None) the size formula will really set it at; one that comes back at BODY_FLOOR_PT wants a wider column, not another build
the_largest_step_this_copy_takes(text, box, *, font=None, bold=False, spacing=1.15, wrap=False, largest=TITLE_PT) the biggest step of the ramp the copy still fits that box at — the only call that answers upwards. wrap=False keeps it on the lines you gave it; wrap=True is for copy meant to reflow. largest is the step to stop at and you name it: TITLE_PT for a label in a shape, LEAD_PT for a line that leads a band, BODY_PT for copy, NUMBER_PT when the copy is the figure. It is not inferred — a character count cannot tell a long word from a sentence
what_this_page_gave_up() every concession the measuring calls made on this page since you last asked, and it clears the list as it answers. A ramp walk that reached BODY_FLOOR_PT and a formula that broke at a separator both come back here: they returned a size rather than raising, so the page looks measured and the copy was quietly cut down to fit. Call it once a page is drawn and read what it says before the build does
card_body_box(box, *, icon=None, title="", title_size=LEAD_PT) where a card's copy starts, once the icon and the title have taken their line
card_size(width, *, icon=None, title="", body=(), size=BODY_PT, title_size=LEAD_PT, font=None) how tall a card has to be for what goes in it — max(card_size(w, **c).h for c in cards) levels a row without padding it out to the page, which is what card_group does for you; reach for this on its own for a group that is not a plain row or column
stack(box).room what is still unspoken for, as a box, without taking it — fits(picture_size(fig, down.room), down.room) is the whole question
stack(box).short_by(*heights) how many inches the whole plan runs over, or 0.0 — measure every band, ask this, then draw. rest() spends the region it answers with; room is the same box and does not
stack(box).slack(*heights) the same arithmetic the other way: how many inches the region has left once these bands are in it
stack(box).centre(*heights) half that leftover above the run, half below, and hands the cursor back to chain. For a single run that sits against something beside it
stack(box).spread(*heights) the leftover becomes the gaps between the bands, so the run ends on the region's bottom edge. A lane of cards, and two columns that have to come out level. Never less than GUTTER between them, so a run with no leftover overruns instead of welding — count the n-1 gaps into short_by and shorten the bands. No skip after it, which overruns by exactly what was skipped

A short label in a big box: ask, do not name a step. Every call above except the last measures downwards. What that leaves out is the label its box is far too big for: naming the smallest step of the ramp puts a stage label at 14pt in a shape 1.25in tall. Two or three characters in a box over an inch tall want the_largest_step_this_copy_takes(label, one.box, font=F). Everything playing one role on the page shares one size, so ask for each and take the min.

Ask lines_needed before you fix a band's height. Copy that runs long does not shrink (§8) -- it runs out of its box and over whatever is under it. lines_needed(title, frame.title.w, size=TITLE_PT) coming back 2 is the row below moving down or the box getting wider, decided before anything is drawn.

ppt_charts answers the same kind of question about a chart, by running it against a slide that draws nothing:

the_smallest_box_a_chart_needs(chart, theme, *data, **knobs) the smallest box this chart takes this data in, as a Box at the origin
whether_a_chart_fits(chart, box, theme, *data, **knobs) whether it would take the box you have at all
what_a_chart_will_do(chart, box, theme, *data, **knobs) the Drawn a real draw would return, with nothing written

A chart's Drawn is a Box — the plot itself, what is left once the category labels, the axis readings and the key have taken theirs — and it carries where (the chart's own scale, so a rule at 80% goes where the chart put 80%), names_not_written and readings_not_written (what it dropped for want of room), marks_not_to_scale (bubbles drawn at the floor instead of to area) and type_pt (the step of the ramp its labels landed on — a reading, never a setting). nothing_was_dropped is those three in one boolean.

A chart that will not take its box raises TooSmall, carrying short: the deficit in inches as (across, down). It raises before it draws anything, so a try/except around one has no wreckage to clear -- and whether_a_chart_fits is that try/except already written.

ppt_charts, always present — twenty-three charts drawn as shapes. Each takes the slide, a Box out of the grid, the theme, and its data; each fills the box, and there is no size, face or colour to pass: type comes off the ramp above and steps down when a label does not fit, colour comes off the theme. Signatures and the data shape each one reads: deck/build/references/charts.md (§6).

ppt_shapes, always present — the 109 Office preset geometries a business page can use, by their DrawingML names, and the two layouts built on them. Signatures in deck/build/references/shapes.md, with §7.5.

ppt_template, only when a template is bound:

prototype(template, number) the template's page number, counting from 1
bundled(name) another bundled template by file name, for a page borrowed from it (§8); found through PPT_BUNDLED_TEMPLATES, which ppt_build sets
clone_page(presentation, prototype) copy a template page into the deck, its words and all -- the one way a template page is put to work; §8
units(container), arrangement(run) the page's repeating units; how a run is laid out
boxes(run) each unit's (left, top, width, height) in inches, page order — a size, not a ppt_layout.Box, so boxes(run)[0][2] is a width and not a far edge
place(unit, box) move one unit; the box is (left, top, width, height) in inches, and a ppt_layout.Box is accepted and converted from its two corners
shape_at(slide, number), drop_shape(shape) one shape by index, counting from 1; remove it
shape_near(container, left, top, tol=0.08, with_text=False) one shape by where the page shows it, in inches
shape_saying(container, prefix) one shape by the copy it starts with
page_position(shape) (left, top) in inches on the page, groups resolved
page_box(shape) where a shape is drawn, as the two corners a region is given in, with the scale its group applies -- so it drops straight into clear_region
shapes_in(container, box, *, share=0.5, with_text=False) every shape drawn in box -- a ppt_layout.Box or a page_box(shape), never four bare numbers -- groups walked into -- shape_near's plural, and the way to find by place rather than by words; share is how much of a shape must be inside to count, and share=0 asks what touches the box at all
clear_region(container, box, *, share=0.5, keep=()) empty box -- Box.corners(...), Box.at(...) or page_box(shape), since four bare numbers cannot say which reading they are and are refused -- so you can draw in it: drop_shape for everything shapes_in finds, returning what it removed and, in left_standing, what still lies over the box. keep spares a shape by the words it shows or by its # [n]
raise_type(slide, floor=BODY_FLOOR_PT, min_chars=COPY_CHARS) lift copy the template states under the readability floor (BODY_FLOOR_PT is 14pt) and let its box grow to hold it
replace_text(target, text, new=None), replace_picture(shape, image, fit="contain", *, anchor="centre", trim=None, zoom=1.0, alpha=None, box=None) in place, keeping how the template set it; alpha washes the new picture the way backdrop does, for the frame that is the page. shape may be a drawing rather than a frame -- a group of freeforms, a cartoon -- or a list of shapes making one: the picture takes its box and its depth and the drawing goes
backdrop(slide, image, *, alpha=1.0, scrim=0.62, ink=None, light_type=True, box=None, anchor="centre", trim=None, zoom=1.0) a photograph behind everything on the page, cover-cropped to the canvas (or box), at full strength under a plane of the theme's ink at scrim, with every run of type on the page set light (saturated accents kept) -- the cover form; scrim=None with alpha at 0.12 or under is the texture form behind a template's own ground; returns the picture
layout_pictures(slide) the photographs a page inherits from its layout, largest first; replace_picture(layout_pictures(slide)[0], image, "cover") changes them for every page on that layout; one the size of the page is the page's background -- alpha=0.1 keeps it the texture the template meant (0.12 to 0.80 is the fog washed_backdrop reports), and a photograph meant to be seen goes in at full strength under a plane of ink with light type, as backdrop lays them
wash(shape, alpha) set any picture's transparency -- a frame the template drew, one replace_picture filled, one you placed -- to the same share backdrop takes; a picture at full strength again is wash(shape, 1); a photograph a title has to read over wants a plane of ink over it, not a wash to 0.3, which reads as fog
add_unit(target, count=1) one more slot on a page's repeating run -- a run from units(slide), or the slide for its longest -- copied from the last unit and laid out again
remove_unit(unit) one slot fewer, and the row closed up; drop_shape removes and leaves the hole
clone_shape(shape, box=None) a copy of one shape on the same page, at a page box -- the clone_panel five builds wrote for themselves

One word in a line, in the accent. write's size, bold and colour style the whole box, so a number that has to carry the page needed a second text box beside its own unit. A paragraph may instead be a list of Runs, each with its own:

write(slide, box, [[Run("营收 "), Run("1.4 亿元", size=16, bold=True, colour=ACCENT_INK)]],
      size=14, colour=INK, font=F, cjk_font=HAN)

Every field but the text falls back to the call's own, the line is measured and its box returned at its tallest run, and this is what no_anchor is asking for when it says to set the number that carries the page two steps up the size ladder.

A veil over a photograph, not a panel. Copy set straight onto a picture is readable where the picture is dark and gone where it is not, and unreadable reports the half it can measure. A veil puts an even ground under the type and leaves the photograph visible; 0.35 to 0.55 is the usual range, and an opaque plane over a picture is the picture thrown away.

picture_fit(slide, PHOTO, frame.body, T)
plane(slide, frame.body, T, tint="foreground", radius=False, opacity=0.45)
write(slide, frame.body.inset(0.6), claim, size=TITLE_PT, colour=T["background"], bold=True)

Take a handle by what you know, and never by shape.left. A template's content is mostly inside groups, and a shape in a group states its position in the group's coordinate space, scaled by the group's own extents — so shape.left is not where the shape is, and comparing it against a coordinate read off the render finds nothing. Three of four live authors wrote that comparison by hand and it is four of their twenty build failures. page_position(shape) is the resolved position, shape_near(slide, 1.56, 2.47) is the search over it, and shape_saying(slide, "Method") is the same search by copy; each refuses by listing what the page does hold.

A cloned page carries the template's type sizes. Where those are under the readability floor, raise_type(slide) lifts them -- run sizes, and the paragraph or list-style size a bare run inherits -- and swaps the autofit that shrank them for one that grows the box instead, leaving copy under 20 characters alone because the floor does not apply to it; it returns how many boxes it touched. It cannot help the other half of type_floor — copy stated at the floor and shrunk by its box — which needs a bigger box rather than a bigger size (ppt_layout.fits, text_size).

With a template bound, the title row is the template's. house_style measures where its own pages put one, so use the box it names — title_row_box_corners_in, or the title_row_as_code line built from it — and decide it once, in the shared setup, with every content page calling that one thing. A page block that computes its own title coordinates is how a deck ends up with three title rows (§8, §12).

Five of those raise a question a signature cannot answer:

  • replace_text(slide, "旧文字", "新文字") finds whatever on the cloned page holds that string and writes it — this is how a template page is filled in, one call per line the page says. The key is the text the shape holds now, which is exactly what ppt_template(pages=[N]) prints above each shape, so a read-back is a list of keys to paste. replace_text(shape, "新文字") writes one shape you already hold. The value may be a list of Run instead of a string, so one word can carry the accent:
s = clone_page(prs, prototype(tpl, 6))
replace_text(s, "单击此处添加文本",
             [Run("访客中约 "), Run("84%", bold=True, colour=ACCENT_INK), Run(" 到访过夜市")])

Whatever a piece does not state is the template's — every piece is a copy of the run the template put there, so the line keeps its face and size and one word of it does not. This is the only way to emphasise a word on a cloned page: a plain string puts the whole line into one run, and one run carries one colour. Pass a list of those lists for several paragraphs.

  • replace_picture(shape, image, fit) — "contain" shrinks the frame to the picture's own proportions; "cover" crops the picture to fill the frame as it stands. Either way it refuses a landscape figure in a portrait frame rather than squashing it. The template's illustration is not always a picture: on a section page it is as often a cartoon drawn as a group of freeforms, with no image to swap. Hand that group (or a member of it, or a list of the loose shapes that make the drawing) to replace_picture and a picture takes the drawing's box and its place in the z-order, the drawing gone -- a member stands for the wordless group around it, so the cartoon goes whole and the card it sits in stays; replace_picture(shape_at(slide, 7), path, "cover") does the same when shape 7 is drawn, and replace_picture([shape_at(slide, 5), shape_at(slide, 6)], path) gives several loose shapes way to one picture. An illustration meant to sit on the template's ground is asked for with ppt_generate_image(..., transparent=true), which has it drawn on a green screen and keys the green out. Three arguments decide which pixels a cover keeps, and none of them writes a file: anchor is the side the crop keeps — "centre", "top", "bottom", "left", "right" — so a photograph whose subject runs along the top survives at anchor="top" where a centred crop cuts it; trim=(left, right, top, bottom) cuts shares off the source's own edges before the fit, which is how a screenshot with a progress bar along the bottom goes on the page (trim=(0, 0, 0, 0.08)); and zoom is a multiple of the scale that just covers the frame, so zoom=1.6 shows 1/1.6 of it — a detail made legible at the size the frame has. Under 1 is refused, because a cover that does not cover is fit="contain". trim works on contain too, where the frame gives way to what is left of the source. Do not pre-crop the file with PIL: a crop written into a new file is a second copy to keep track of, it goes past the proportion check here, and the template's own stale crop stays on the shape.
  • units(container) returns runs of repeating sibling groups — a card row, an agenda list.
  • arrangement(run) returns ("row"|"column"|"grid"|"irregular", rows, cols). Nothing moves the survivors for you: closing the hole four units leave on a 2x4 grid is a design decision, and place is how you make it. The same holds for growing: add_unit extends a row, a column or a grid at its own pitch and refuses an irregular run -- two cards set diagonally, pills on a curve -- because where the next one goes is yours to say. Say it with clone_shape(run[-1], (left, top, width, height)) and put the copy where the design wants it (the refusal prints where the existing units sit); then the copy takes replace_text like any other slot.
  • remove_unit(unit) takes one slot out of a run and closes the gap, group and all. That is how a page with more slots than the deck has points is cut down to size, and it is the only operation replace_text cannot express on its own.

Re-flow off the geometry the template already fixed. boxes(run) hands back the pitch and the size the template drew, and that tuple is exactly what place takes; computing a position without it is guessing at coordinates the page already holds.

ppt_theme gives THEMES and rgb. ppt_icons gives add_icon(slide, name, left, top, size, colour, width_pt=None) -- the pen is a twelfth of the side, the icons' own 2-on-24 weight, unless width_pt fixes it -- swap_icon(slide, shape, name, colour=None), find_icons(term) and ICON_NAMES.

Repeated units, in detail. A content page is usually one small group repeated, and each repeat is written like any other line: replace_text(slide, "the words that unit holds", "yours"), once per line. ppt_template(pages=[N]) prints those words for every shape on the page, unit by unit, so the read-back is the list of keys.

Count as you read the render: units top row first and left to right, and inside a unit the same. That is the reader's order, not the file's -- a card's 60pt number sits above its heading whatever order the designer happened to save it in. It matters for reading the page back and for units(slide), which returns the runs in that order.

A slot fewer, and a slot more. remove_unit(unit) takes a slot out and closes the row up -- three cards left on a four-card row are re-spaced across the row's original width, a 2x4 grid losing two closes to six. drop_shape removes and leaves the hole, which is what you want for a single shape and never for a slot. add_unit(run_or_slide) goes the other way, copying the run's last unit and laying the run out again -- pass units(slide)[0] or the slide for its longest run. Both refuse a run that follows no grid, because where the next pill on an S-curve goes is the design's to say; there, clone_shape(run[-1], (left, top, width, height)) puts the copy where you say and the refusal prints where the existing units sit.

A number is the template's, and it does not renumber itself. Cut an eight-slot agenda to six and the survivors still read 01 to 06 because they were the first six; cut from the middle and you write the numbers yourself, one replace_text each.

s = clone_page(prs, prototype(tpl, 2))            # an agenda page of eight slots
for old, new in zip(TEMPLATE_LINES, sections):    # six of them take this deck's sections
    replace_text(s, old, new)
for spare in max(units(s), key=len)[6:]:          # the seventh and eighth go
    remove_unit(spare)

A page can have two runs. units(slide) returns them longest first, and a template's four-card page is often two runs of two (cards 01-02 and 03-04, grouped in pairs). Nothing special is needed -- replace_text is keyed on words and does not care which run a shape sits in -- but check units(slide) whenever the render shows more repeated cards than you wrote, because that is the page telling you there are slots you have not reached.

Some pages have no run at all that units can see. Content cards drawn as loose shapes are not detected as a repeating unit, so units(slide) may return only the icon badges beside them. replace_text reaches the cards anyway, because it asks the page what it says rather than how it repeats. This is the reason the route is words-first (§12, and the note under remove_unit): on such a page only remove_unit needs a run, and only if you are cutting slots.

5. Figures come from the sources

ppt_ingest extracts them and nothing generates them. Never generate a substitute for a real product UI, logo, person, scientific result, published figure or statistical claim.

It reads PDFs, text, HTML, CSV, images, and office documents -- .docx, .xlsx, .doc, .odt -- by converting those to PDF first, which needs LibreOffice. Anything it could not read comes back as a finding naming the file: a source that is not among the deck's evidence is one you must not write pages as though you had read.

Look before you place. ppt_figure_inspect returns a figure as an image with the label its own source gave it, the width past which the bitmap softens, and the concerns the extraction recorded — measured sentences you can act on or overrule, not a verdict. Unseen, you cannot tell a legible plot from a scanned blur, a figure from a logo that survived extraction, or a composite you should be cropping.

Cite the label the source printed, and only when that figure is the one on the page. A page captioned "Fig. 4" showing Figure 5 is refused (§12).

Keep source notes short. One compact line at the bottom, such as 来源:EverOS 官网;Mem0 官方文档。 A source note is a pointer, not a paragraph: URLs, methodology caveats and analytical conclusions go in the body or the speaker notes.

A table in the sources is evidence to read, not a figure to place: retype it (§6).

A picture set flush against a panel fights the panel's outline. Inset it — box.inset(...) gives the picture the padding the panel's own copy has.

Never distort aspect ratio, crop away interpretive labels, include a neighbouring caption by accident, duplicate a printed caption, or leave transparency composited onto black. If evidence is unreadable at the size it has: crop to the panel you cite, give it more of the page, or rebuild it from the exact values.

Fetched pictures are first-class, not a fallback. Reach for the two paths of §2 whenever a page names something with a face -- a product, a company, a standard, a published chart -- not only when the materials left a hole; their results carry pixel dimensions, so a mark that would land soft can be rejected before it is placed. ppt_fetch carries the URL with the picture so a page can credit it, and the listing's own caption or alt text passed as ppt_fetch(caption=...) travels into the same catalogue field a paper's figure fills. Copy the page's words, never your reading of the picture.

Generate at the shape of the frame it goes in. aspect_ratio is the picture's shape, and replace_picture(..., "cover") crops whatever does not fit: a 16:9 picture in a banner strip keeps its middle third, a 16:9 picture in a portrait column keeps a sliver. Choose the ratio nearest the frame (3:2 or 4:3 for a photo frame, 9:16 or 3:4 for a column, 16:9 for a wide band) and say in the prompt where the subject sits, so what the crop keeps is the subject.

Plan every picture the deck will generate and ask for them in one call -- ppt_generate_image(prompts=[{prompt, filename}, ...]) makes them at the same time and ingests them once. Generate with ppt_generate_image only after both paths find no suitable existing visual; a generated image illustrates a concept and never replaces evidence. Ask at the shape of the region it will sit in — aspect_ratio takes 16:9, 4:3, 3:2, 1:1, 2:3, 3:4, 9:16 or 21:9 and defaults to 16:9, so a portrait strip gets a landscape image to crop unless you say otherwise; a model that draws only a few frames answers with the nearest one, and the reply's width and height say what came back, so fit it to the box rather than assuming the ratio. The picture is drawn by whatever image model the host configured, not by a fixed one: say the manner in words the model can act on. references hands it pictures to match or vary -- the template's own illustration for its manner, an earlier generation for a consistent series, a user's photograph to restyle. Fetched numbers are another matter: those are a source the user did not choose, so anything the deck states as fact still comes from the materials.

A cover does not need a figure. Its job is the title, who wrote it and where, and a paper's Figure 1 pressed into its corner is smaller than the page it will get later.

The cover and the closing page keep the template's composition. Their title block, chips, marks and rules are the design; a photograph or illustration in its own frame beside them is the placeholder. Put the page's picture into that slot -- replace_picture(layout_pictures(slide)[0], image, "cover") when the slot is on the layout, replace_picture(shape_at(slide, n), image) when it is on the page -- and never drop the layout's art to make room for a full-bleed photograph (drop_shape refuses a layout shape). Where the layout's picture is the whole page -- border, ground and illustration in one bitmap, as the bundled templates draw their covers -- it is the frame itself: keep it, and the second reader is told so. What goes into the slot is decided by what the template put there, and the template can be anyone's -- a bundled one, or a deck the user uploaded. Look at the slot in the render before you ask for a picture: a slot that holds a photograph takes a photograph; a slot that holds a drawing takes a cut-out in that drawing's manner, ppt_generate_image(..., transparent=true) with the manner described from what you see (flat vector, line art, 3D render, watercolour; its palette; its outline weight), even for a real place, and with the template's own illustration passed as references so the model matches it rather than your description of it. Never name a template or a house style you have not seen on the page. A template whose cover is a photograph takes backdrop.

A backdrop is the one generated picture that never poses as evidence. A cover, a contents page, a section page or a closing page wants atmosphere more than a figure, and a template's own photograph there is a placeholder. All four take one where an image key is configured -- the contents page included, which is the one that gets forgotten and comes out as a list on a white field. Generate one with ppt_generate_image at the page's shape (aspect_ratio="16:9" for a full page) and lay it behind everything with backdrop(slide, FIGURES / "cover.png"): the photograph at full strength under a plane of the theme's ink, with the page's type set light -- the form a reference cover that works uses. A photograph washed to 30% on a white page under black type reads as fog, and the build reports it (washed_backdrop). Write the prompt as subject, scene, composition, style, palette and what to leave out (no text, no logos, no faces).

The style is the subject's first and the template's second. A real place, a street, a market, a crowd, a product, a building, a meal -- anything the audience could stand in front of -- is a photograph: photographic, natural light, 35mm, no illustration, and the template's palette only in the grade (warm amber light, cool teal shadows). An idea that has no face -- a process, a principle, a mood for a section page -- takes the template's own manner, and an illustrated template draws it as an illustration. A cartoon night market on a proposal to a district government reads as a children's book; a photograph of one reads as evidence, which is why the real thing is found first (§2) and generated only when it cannot be. Within the deck, one manner per kind: every place photographic, every concept in the template's hand -- a photograph on one case page and a flat drawing on the next read as two decks. A faint picture behind a template's own ground is the texture form, backdrop(slide, image, alpha=0.1, scrim=None), and it stops being texture past 0.12. Look at the render (§10): the contrast reading is taken off the pixels, so type that does not carry over the picture comes back as unreadable type. A body page dense with cards does not want one. The same knob exists for a picture already on the page: a photograph the template drew or replace_picture filled that a title has to read over wants a plane of ink over it and light type, the way backdrop lays one -- not wash(shape_at(slide, n), 0.3), which is fog; a picture at full strength again is wash(shape, 1).

Look at the page before you ask for its picture

A generation happens while the page is still a plan -- prepare, brief, template, pictures, then the program -- so a picture asked for at that moment is asked for against nothing. Draft the page first: write its block, ppt_build(slides=[n]), and read the render. What you ask for is then what the page lacks, not what it already carries.

Hand the model the material; do not describe it. references takes up to six pictures -- the logo file, the mark the brand actually uses, the template's own illustration, an earlier generation the new one is a series with -- as figure ids, source file names or absolute paths. A described logo comes back as a different logo.

Say where the type goes, and ask for no type. Name the region the title needs by side and by share -- "the left 55% of the canvas stays almost pure dark, reserved for title text overlay" -- because a subject centred in the frame leaves the title nowhere to sit. And no text, no letters, no numbers: generated lettering is wrong in every language, and every word on a page is set by the typography.

Then look again. The picture was for a page, so rebuild that page and read the render: only the composed page counts, and whether the type carries over what you laid down is read off those pixels and refuses (§12).

Where the deployment has no image key, ppt_generate_image says so -- no image API key is configured -- and that is an answer to pass on, not a page to leave open. Carry it on type, grid, rule and colour, and say which pages would have had a picture. A blank slot, a flat colour block standing in for one, or a body page's photograph moved onto the cover are three ways of not saying it.

Tables are for figures, and a deck has few of them. A table earns its grid when a reader compares numbers down a column -- a benchmark, a price list, a scoring matrix. Four labelled points, a list of measures with a sentence each, a plan with phases: those are cards, a timeline or a list, and set as tables they turn a deck into a report. One 20-page deck that set five of them read as five spreadsheets; hold a deck to about one table per section, and ask of each whether the reader compares figures in it or only reads it.

6. Charts and tables, drawn

Charts are shapes — rectangles, hairlines, marks and labels — which is what keeps them editable and on the deck's palette. matplotlib imports in the program (the engine uses it to typeset formulas) and is not how a chart is drawn here: a plot pasted as a picture is off the palette, cannot be edited in the deck, and its labels are pixels the type measurements never see.

ppt_charts draws twenty-three of them for you, and the mapping from a value to a length is the half you must not write by hand. Import what the page needs:

from ppt_charts import column, horizontal_bar, waterfall
column(slide, frame.body, T, [("East", 185), ("South", 142)], accent="East", unit="M")

The signatures are in §4.

Which form, and what to draw when none of them can be

deck/build/references/charts.md carries the rest: the twenty-three signatures, a row per form saying what it encodes (reference, not a constraint), and what to substitute where the shape wanted cannot be reached from rectangles and straight lines at all — a pie, a gauge, a radar, a sankey, an area chart. Open it before choosing a form.

The twenty-three are shortcuts, not a ceiling. Their base is public on ppt_charts too — the ink (rect, disc, ring, hline, vline, poly, write_label), the scale (span, snap, linear), the palette discipline (series_paints, stack_paints, shades, emphasis, ink_on, contrast) and the label fitting (type_face, text_width, pick_size, line_height). Where none of the forms is the shape of what the page argues, write the form; the rules below still hold. A chart you write that raises TooSmall and returns a Drawn also works with whether_a_chart_fits, what_a_chart_will_do and the_smallest_box_a_chart_needs.

The rules a drawn chart obeys

  • Length is the value. Every bar, segment and track is exactly proportional, and a length axis starts at zero. A misdrawn axis is caught only by you looking at the render (§10), which is the reason to let ppt_charts map the values.
  • Default — label the mark, and leave the legend to the plot where no label can reach its own. Name the units, and put the axis maximum where the reader can see it.
  • Default — one thing is accented. The bar, segment or point carrying the claim takes the accent and the rest take muted or grid; how many colours a chart ends up with follows what it encodes, not a quota. SERIES in order is for a genuine multi-series plot, and its later colours are quiet on a light ground, so give them the supporting series.
  • Baselines and axes are hairlines. The charts draw their own (§3 for what rule is and is not). No vertical gridlines, no frame around a bar chart, no ground behind a plot unless it separates layers.
  • Never invent a value. No placeholder, no number reconstructed off a plot you cannot read, no interpolated point. Where the series has a hole, the page says so.

For the compact bars beside a table or under a claim, a row of rectangles beats any chart object: you control the length, the colour of the one bar that matters, and where its value sits — horizontal_bar in a body.rows(3)[2] is that page. Do not reach for add_chart: it arrives with Office's own six colours and gridlines, as recognisable a tell as any stock template.

Never draw a table with a bare add_table, which arrives with Office's own look — a white hairline around every cell, banding on, and a header style that fights the deck's palette. A screenshot of a source table is no better: another typeface, and it cannot be reweighted around the page's conclusion.

Everything above that ban is yours. ppt_layout.table() is the shortcut for the ordinary comparison table — the Office look taken off, plus the part that is arithmetic rather than design: it sizes each column from what that column holds, measures each row from the lines its cells really wrap onto, and spreads the rows into the box you gave it. Its look is defaults, and every one is a keyword away (§4). Or draw the table yourself out of Box, text_size, write and plane, the right call whenever the page wants a grid table() does not draw: merged cells, a header spanning three columns, an icon inside a cell, a sparkline down one. Both paths, a worked hand-drawn table and the seven mark kinds are in deck/build/references/tables.md. Open it when a page carries a table.

Compose freely around one: a two-product comparison may be two parallel regions with the same row labels rather than one table, and a conclusion may sit on its own baseline below.

Preserve units, scales, qualifiers, series meaning and source labels exactly.

6.5 The page's frame, and the helpers that fill it

from ppt_layout import PAD, card, card_body_box, card_size, fits, heading, page, plane, stack

frame = page()
heading(slide, frame, T, "把任务定义抽象成查询,网络本身就与任务无关",
        "02 统一范式 · 任务切换 = 换一组输入查询")
said = [("route", "分类头没了", "类别成了网络的动态输入,语义表示只通过损失监督学到。"),
        ("layers", "权重只有一套", "推理时按需拼装查询集合就能热切换任务,不重训。"),
        ("gauge", "代价", "四类任务合并统计 46.3 mAP,比四套权重各自训练高 0.2。")]
lanes = frame.body.inset(PAD, 0.0).columns(3)
tall = max(card_size(lanes[0].w, icon=i, title=h, body=b, font=FACE).h for i, h, b in said)
cell = stack(lanes[0]).take(tall)
if not all(fits(b, card_body_box(cell, icon=i, title=h)) for i, h, b in said):
    tall = max(card_size(lanes[0].w, icon=i, title=h, body=b).h for i, h, b in said)
frame = frame.holding(tall + 2 * PAD)     # the band is measured, so the body is cut to it
ground = frame.body
plane(slide, ground, T, tint="surface")
for box, (icon, head, body) in zip(ground.inset(PAD).columns(3), said):
    card(slide, box, T, tint="background", icon=icon, title=head, body=body,
         font=FACE, cjk_font=HAN)

The body is the room a page may use, not the run it uses. page() hands back all of it, this page uses one measured band, and everything left over stays at the foot unless the page says so: frame.holding(tall + 2 * PAD) cuts the body to the run and splits the leftover as air above and below it. Ask it once the bands are measured and before a cursor runs down them -- the kicker, the title and the footer do not move, so it reads the same before or after heading, and a body the run already fills comes back unchanged. Pass the bands and the gaps between them in the order they occur, and pass the taller run where two lanes differ. Not for a run something else already spreads into the whole body: card_group(..., down=True) given the body puts the leftover between its own cards, and a body cut to the sum of their heights first leaves them touching. More on where the page's slack goes below.

heading() is one ready-made top edge — a quiet ground behind the title row with the kicker and the title set on it, stopping short of the body, and bleed=False to keep that ground inside the safe area. It sets the title in page()'s own box rather than the template's, so reach for it where you are composing a page yourself (§8) and where that box agrees with the title_row_box_corners_in the template was measured for (§4).

The header is one compact group. A section tag, title and one explanatory line sit close enough to scan as a single unit; the larger vertical break belongs between that unit and the body. Tighten those internal gaps when the actual title runs to fewer lines than the template's example did.

Two claims in one frame read as one paragraph with a line break in it, and putting a mark in front of each does not change that reading. A mark is not a structure. A single paragraph of explanation is a paragraph and nothing here is about one; what the build reports as listed_claims is a box carrying two or more parallel claims (§12).

What a card holds, and what it costs, before it is drawn. card paints a rounded plane in the tint you name and sets the icon's square, the title bold beside it and the copy under it, all inside PAD (§7). It fills whatever box you hand it, so four cards off frame.body.columns(4) are each as tall as the body. card_size(width, icon=, title=, body=, font=FACE).h is how tall one has to be for what goes in it, and max(...) across a set levels a row without padding each card out to its cell; fits(body, card_body_box(box, icon=..., title=...)) says whether the copy went in. Both come off one estimate, which reserves for the widest face it knows when told none -- so where the unnamed call refuses the levelled height, take the height it asks for.

Dividing a region. columns, rows and box.grid(cols, rows) cut a box into cells, row-major; a stack given gutter=(box.h - n * tall) / (n - 1) spends the region's slack as air between its bands instead of leaving it in a heap at the bottom. plane(slide, box, T, tint="surface") paints a region, and it counts towards evidence like any other filled shape (§12).

points(slide, box, theme, items, numbered=False) writes a marked list, a hanging mark per item and numbered=True for an ordered one; points_size measures one first, which text_size cannot.

Never guess a y coordinate. stack(box) is a cursor down a region: take(h) hands back the next band and moves on, rest() hands back everything still unspoken for, and skip(GUTTER) puts the deck's one gutter between them. Bands are adjacent otherwise, so heights from table_size and the_smallest_box_a_chart_needs add up to what the region has. A coordinate you were given is different and you should paste it: house_style's body_area_as_code (§8) is measured off the template.

Full inside, air between. A component is measured and given exactly that much (card_size, text_size, picture_size, table_size); the room the page has left over goes between components, never inside one. A cursor runs from the top of its region, so left alone it does the opposite -- every unused inch piles up underneath and the page has a band of white along its bottom edge. Three calls spend it, all of them before the first take:

  • stack(box).spread(*heights) -- the leftover becomes the gaps, and the run ends on the region's own bottom edge. A lane of cards, a table over a chart, and two columns given the same region, which is what makes them end level. The gap it leaves is never under GUTTER: components any closer read as one unfinished shape, so bands with no leftover to share have to be shortened by the gaps between them rather than run flush against each other. card_group(..., down=True) is this call with the cards' heights measured for you.
  • stack(box).centre(*heights) -- half above and half below, for a run that nearly fills its region and sits against something taller beside it.
  • page().holding(*heights) -- the page's own leftover, taken out of the body before a cursor runs down it at all: the body is cut to the run these heights add up to, and what is left becomes air above and below that run instead of a band along the foot. Not half and half -- the white over the body is the GUTTER under the heading and the white under it is the MARGIN, so an even split still leaves 0.44in more at the foot; the two are measured from the heading's edge and to the page's and come out reading equal. This is the call when the page's bands were measured and taken off a cursor -- a table over a card row, a figure over its conclusion -- because that is where the leftover has nowhere else to go: spread into the one gap between two groups instead, a whole page's slack is reported as a blank field in the middle rather than one at the foot. It moves the region and not a cursor, so columns, split_left and a stack down each lane all come out of the same corrected band and two lanes still end level; where the lanes differ, pass the taller run. Not for a run something else already spreads into the whole body: card_group(..., down=True) given the body spends the leftover between its own cards, and a body cut to the sum of their heights first leaves them touching. A body the run already fills comes back unchanged, and so does one the run overruns -- growing it would put type through the safe margin, and an overrun is take's to refuse with both numbers.

Spread components, never running copy. The gap between two cards is air; the gap between two paragraphs is a break in a thought, and three sentences pulled 1.9in apart stop reading as one column at all -- rendered and compared, it is worse than leaving them at the top. So when a copy column comes out far shorter than its region, neither call is the answer: that is a page bigger than what it holds, and it is answered by turning the copy into components (the same three sentences as three cards fill the column and land level with the figure beside them) or by the page having more to say.

And a chart is as tall as the box you hand it. A bar's length is the reading, so a chart given a short box draws short bars and leaves the page empty under them -- the same page then looks crowded and unfinished at once. Give it the band (down.rest(), or the height short_by says is free), not a box guessed at the size the chart "should" be.

The shape most pages want, in four lines. An unequal split, the narrow side laid out to the body's own floor, one of its bands divided again, and a group on every surface. It answers all four of the things a reader complains about at once -- both columns end level, the columns come off one grid, every group has an edge, and the page ends where it means to:

frame = page(footer=True)
write(slide, frame.title, title, size=TITLE_PT, colour=T["foreground"], bold=True, font=F, cjk_font=HAN)

left, right = frame.body.split_left(0.545)          # the picture side and the reading side
picture_fit(slide, img, left, T, font=F, cjk_font=HAN)   # no caption strip: it goes in the foot

PLAN = (1.05, 1.05, 0.86, 0.86)                     # measured, then checked:
assert stack(right).short_by(*PLAN, *((GUTTER,) * (len(PLAN) - 1))) == 0.0
down = stack(right).spread(*PLAN)                   # the leftover becomes the gaps
claim, pair, rows, note = (down.take(h) for h in PLAN)

for box, tint in ((claim, "foreground"), (rows, "surface"), (note, "accent_soft")):
    plane(slide, box, T, tint=tint)                 # every group on a surface of its own
for cell in pair.columns(2, gutter=0.15):           # one band divided again, same grid
    plane(slide, cell, T)
footer(slide, frame.footer, T, note=f"{source};{cap}", font=F, cjk_font=HAN)

short_by counts the bands and not the gaps between them, so pass the n-1 gaps in with them -- spread never puts less than GUTTER between two components, and a plan that fits without them still raises at the band where the room runs out.

Measure every band, ask short_by, then draw. take refuses a band the region cannot hold, and it refuses at the band that asked -- which is the last one, not the one that was too tall. So a plan checked band by band as it is drawn fails at the bottom of the page with nothing said about the top, and the fix is never where the refusal is. Measure all of them first, hand the heights to stack(box).short_by(*heights), and it answers how many inches the whole plan runs over, or 0.0. Take that off the bands that can give it up -- the ones whose content is not the page's claim -- before the first take. This is the most common way a build script dies.

Give a content page its foot -- on the pages you compose. With a template bound the foot follows the template: where its own pages carry none, add none, and a photograph's source lives in the figure catalogue, not in a credit line squeezed onto the page. page(footer=True) then footer(slide, frame.footer, T, note=...) on every composed page that is not the cover, a divider or the closing. Two things come of it and neither is the number: a body that ends two thirds down an otherwise blank page reads as unfinished, and a rule across the foot is what says the page ends there because it was meant to. The number is a slidenum field, so inserting a page ahead of it does not make it wrong.

frame = page(footer=True)
...
footer(slide, frame.footer, T, note="来源:公开报道整理", font=F, cjk_font=HAN)

A cloned page can have a foot, but look at the strip first. Cloning a template example leaves you no Frame, and the strip does not need one: it is a fixed box on the canvas, and page().footer and page(footer=True).footer are the same Box(0.72, 6.48, 12.61, 6.78). What it does need is to be empty. Measured across the bundled templates, most of their example pages use that band themselves -- 18 of 25 pages of one and 14 of 18 of another have a shape whose ink reaches into it, and one page's reaches 7.68in, past the canvas. Drawing a foot there puts the note over the template's own design; rendered, the source line came out across a pill.

So measure the page you cloned before writing into its foot, and where the band is taken, the template's furniture is the page's furniture -- leave it, and put the source in the one page of the deck that does have room, or on the page you composed yourself.

slide = clone_page(prs, prototype(tpl, 7))        # the template's page; replace_text per line
strip = page().footer
taken = shapes_in(slide, strip, share=0)           # anything of the template's reaching in
if not taken:                                      # the band is the template's, or it is yours
    footer(slide, strip, T, note=source, font=F, cjk_font=HAN)

A template that paints its own band across the foot needs colour= for a tone that reads on that band — the theme's muted is chosen against the page's ground, not against a band the master drew.

Then ask what the page gave up. The other way a plan goes wrong is quieter: the measuring calls answer rather than refuse, so a ramp walk that ran all the way down to BODY_FLOOR_PT and a formula that had to break at a separator both hand back a value and the page looks measured. what_this_page_gave_up() is the ledger of those, cleared each time it answers -- call it once the page is drawn and read it before the build does, or the first you hear of the concession is a type_floor on a page you thought you had fitted.

plane(slide, band, T)
points(slide, band, T, rows, size=BODY_PT, font=F)
for gave in what_this_page_gave_up():
    print(f"  gave up {gave['what']}: asked {gave['asked']}, got {gave['got']}")
rows = [["任务", "四套权重", "一套权重"],
        ["实例分割 VIS", "46.1", "46.3"],
        ["语义分割 VSS", "52.4", "52.6"],
        ["全景分割 VPS", "50.8", "50.9"],
        ["指代分割 RVOS", "61.2", "61.4"],
        ["四类合并", "46.1", "46.3"]]
answer = "四类任务一套权重,切任务只换输入查询。"
fig, cap = f"{FIGURES}/fig2.png", "Figure 2:架构(论文原图)"
grid = table_size(rows, T, size=BODY_PT)
said = text_size(answer, frame.body.w - 2 * (PAD + 0.10), size=LEAD_PT, bold=True, font=FACE)
share = 1 - grid.w / (frame.body.w - GUTTER)
above, _ = frame.body.split_top(1 - (said.h + 2 * PAD) / (frame.body.h - GUTTER))
figure, _ = above.split_left(share)
down = stack(frame.body)
tall = max(grid.h, picture_size(fig, figure).h)
figure, lane = down.take(tall).split_left(share)
picture_fit(slide, fig, figure, T)
table(slide, lane, rows, T, size=BODY_PT)
down.skip(GUTTER)
band = down.take(said.h + 2 * PAD)
plane(slide, band, T, tint="accent_soft")
write(slide, band.inset(PAD + 0.10), answer, size=LEAD_PT, bold=True, colour=INK,
      font=FACE, cjk_font=HAN, anchor="middle")

take(said.h + 2 * PAD) and not rest(), and that is the whole of what keeps this page from being a void. rest() hands back everything still unspoken for, so a tinted plane drawn on it is as tall as whatever the bands above happened to leave -- and this one holds one line. said.h is already in hand two lines up, which is the point of measuring: the band is cut to the sentence, and room left over stays unpainted rather than being covered. Copied without that, this fragment is a colour panel several times the height of the copy inside it. Reach for rest() when what goes in the band is sized to the band by construction -- a picture_fit filling it, a table with weights -- and never for a plane with a sentence on it.

Cut the region to what goes in it, in both directions -- and cut the band to the figure, not the figure to the band. table_size(rows, T) with no box answers both directions, and the lane and the band above are cut to its two fields (§4), the band taking the larger of its .h and what picture_size says the figure needs. picture_fit scales an image to fit its box whole without cropping and centres what is left, so a band chosen by eye leaves a strip of white above the figure and another below; picture_size(fig, region) reads the image's own pixels, and handing that .h to stack.take leaves the centring nothing to centre in. A lane chosen by eye fails the other way: what sits in it prints past its own edge, which the file measures as fitting and only the render shows.

7. Icons, on the cards and on the blocks beside them

from ppt_layout import card
from ppt_icons import add_icon, find_icons, ICON_NAMES   # 1304 Tabler Outline names

card(slide, box, T, icon="target", title="语义查询是必要的",
     body="去掉 Qsem 改用线性分类头:YouTube-VIS 44.7 对 46.3")
add_icon(slide, "clock", Inches(0.7), Inches(2.1), Inches(0.42), ACCENT)

On a cloned page the icon is the template's, and swap_icon changes it. A template's icon is a freeform path holding no text, so replace_text passes it by and a page about supply chains keeps the trophy the template drew. swap_icon(slide, shape, name) draws name where that shape was, as large as it was and in its colour -- a theme slot stays a theme slot, so the icon keeps following the deck's palette -- and removes the old one. shape is what shape_near(slide, x, y) or shape_at(slide, n) found, the # [n] numbering of the read-back page:

from ppt_icons import swap_icon
slide = clone_page(prs, prototype(tpl, 12))       # then replace_text per line
swap_icon(slide, shape_near(slide, 6.2, 2.0), "truck")        # the second badge's glyph

It refuses a shape bigger than an icon: a photograph wants replace_picture, a panel drop_shape.

card(icon=) takes one, card_group carries it into a whole row or column of them, and so does a block drawn without a card. Not every block needs one. Pick the icon for what the block argues, not for a noun in its title. An icon takes the card's first line whether or not a title shares it, so what room the copy has left is card_body_box's answer (§6.5).

add_icon is for the icons that are not on a card: beside a kicker, in the corner of a metric, at the head of each band of a hand-drawn panel (§6.5). Stroked vectors, so they scale and stay editable after export, in whatever colour you pass. The ink is inset in the square by no fixed fraction -- from nothing of its height (minus, a rule) to nearly all of its width (json) -- so a row of icons centred on their squares is not centred on its ink. the_ink_an_icon_covers(name, size) is the box the strokes really cover, and add_icon hands back that box measured off what landed.

Check the name before you write it, with a shell rather than a build. Every name is a key in deck/build/icons.json, so grep '"wave"' deck/build/icons.json answers in a shell and costs nothing:

python3 -c "import json;print([n for n in json.load(open('deck/build/icons.json')) if 'wave' in n])"

A guessed name is not free. add_icon raises LookupError on a name that is not there, and a raise ends the whole script -- every page after it goes unwritten and the build round is spent. The refusal does name the nearest three, which is why guessing feels cheap; it is cheap only if the guess was the last thing the script did.

Search by meaning, not by filename. find_icons("deadline") returns calendar_due and find_icons("risk") returns warning: every icon carries its upstream tags and shelf and the search reads them, so the word you would use on the slide is a good enough query. It is a function in the build script, so it answers on the next build -- put every icon you are unsure of in one print(find_icons(...)) and read them all from one round. Ask one concrete word at a time: inventory reaches building_warehouse through its tags, while an abstraction of several words -- supply chain, throughput -- matches on neither name nor tag and comes back with nothing usable or nothing at all. A few hundred names need no lookup at all, grouped by what they are for in deck/build/references/icons.md.

Keep them small -- an icon is a mark beside type, so the square follows the line it labels rather than a size of its own -- and give each real space. On a card that arithmetic is card()'s and not yours.

7.5 Processes, flows and timelines

A sequence is drawn with the shape that means sequence: five rectangles with gaps between them is a list, and a spine with five stops on it is a process. timeline, connect and preset are how, and never by computing the geometry yourself.

There is no chevron_row. Its band of interlocking arrows arrived on every deck, and it could not line up with anything under it -- interlocking steps advance by (width - notch) where box.columns(n) advances by width + gutter, so copy beneath the row sat on a second grid. preset(slide, box, T, "chevron") still draws a single arrow. deck/build/references/shapes.md has the signatures, the label box to write into, and the knobs that are angles in degrees rather than fractions. Open it when the page has a sequence, a branch or a route.

8. Inside a user's template

Its master, theme, layouts and canvas are the deck's house style, and a deck whose theme is not the template's is refused (§12).

A template has structural pages and editable content examples. For a content page, start from the nearest example by information shape: replace its text and pictures, delete spare repeated units, and move or resize the surviving regions when the content needs it -- a prototype is a starting composition, not an immutable form. Where no example carries the page's information shape, compose the page instead (below) -- for at most a quarter of the content pages: the outline refuses a plan that composes more than that, because a deck that redraws its pages inside a template has stopped being the template's however carefully it measures the house style. Measured: 3 of 17 composed read as the template's own; 6 of 17 read as "drawn by hand and ugly", and the six were exactly the composed ones; 17 of 17 read as "nothing but self-drawn layouts".

Even a page you compose starts on the template's page. Clone the nearest example, remove_unit the units the page does not need, and draw into the region that frees -- the header row, the panels, the marks and the ground stay the template's, and only the body is yours. A page drawn from primitives alone carries none of them, and reads as drawn by hand. ppt_template(pages=[n]) gives the example's real geometry to draw against.

ppt_template(project=..., pages=[4, 5]) reads an example page back as the python-pptx that would draw it — up to six pages a call, and as many of them as fit one reply; the ones left out are named in pages_not_read, so ask for those in a second call. It is flat and literal: every position in inches, inherited sizes and colours resolved, groups opened, and a # [n] above each shape, which is the numbering shape_at uses. The page's own pictures are written into the build directory beside your script, so an add_picture("template_00.png", ...) line in it runs as pasted, and the imports it needs head the block as comments. Read a page this way when you need its real numbers -- a card row's pitch, the title's exact box, the accent it really uses. What it cannot reproduce is emitted as a comment naming itself -- a custom-drawn shape, a gradient, a pattern fill or a semi-transparent fill -- and those are the pages to clone rather than redraw.

A chart on a template page is the exception to that clone. The layout may be cloned, but the chart must be redrawn with ppt_charts from your own data and placed in the same position; the read-back page and the line beside the render both name the box it occupies. Cloning it keeps the template's own numbers and nothing here rewrites them: a chart's labels live in its own part rather than in a text frame, so replace_text does not reach them, and no gate reads inside a chart either -- a page shipped this way shows the template's own categories and nothing refuses it. Drop it off the clone -- drop_shape(shape_near(slide, left, top)) -- and draw yours into that box.

The pages you clone

ppt_template names them — house_pages: {"cover": 1, "agenda": 2, "section": 3, "closing": 11} — and renders them. Those four are what a reader recognises the house by: a deck that draws its own cover announces itself as not the user's before a word of it is read.

from ppt_template import clone_page, drop_shape, prototype, remove_unit, replace_picture, replace_text, shape_at, units
tpl = Presentation(os.environ["PPT_TEMPLATE_SOURCE"])   # the original, with its example pages

s = clone_page(prs, prototype(tpl, 1))           # prototype(tpl, N) counts from 1, like the menu
replace_text(s, "科幻未来·推动行业革新", "项目标题")     # the key is the text that shape holds NOW --
replace_text(s, "探索通用行业创新创业新机遇", "本次汇报副标题")  # ppt_template(pages=[1]) prints it
replace_picture(shape_at(s, 7), FIGURES / "selected-figure.png", "cover")
drop_shape(shape_at(s, 3))                       # a line this cover does not use
                                                 # "20XX.XX.XX" left alone stays as written

s = clone_page(prs, prototype(tpl, 2))           # the contents page: one unit repeated
replace_text(s, "目录", "目录")
for old, new in (("第一部分标题", "市场现状"), ("第二部分标题", "需求侧"), ("第三部分标题", "落地路径")):
    replace_text(s, old, new)                    # one call per line the page says
for spare in max(units(s), key=len)[3:]:         # the slots this deck has no sections for
    remove_unit(spare)                           # removed and the row closed up

Every line is named by the words it is holding. There is no by-role shortcut and none is needed: ppt_template(pages=[N]) reads the page back with the text of every shape printed above it, and each of those strings is a key replace_text takes. Read the page, then write one call per line. Writing "目录" and "Agenda" back as two new boxes over the clone instead is the one construction template_underlay refuses.

A string that matches nothing raises, and lists what the page does hold. That listing is the fix: paste the string it prints. Matching is on the words in the box and never on a shape's name, so drop_shape(shape_saying(s, "Presenter name")) removes the cover line that says that, while a real shape name out of the file matches nothing.

Drawing into a cloned page means clearing the space first. replace_text swaps words where they stand, and that is all it does; the moment a chart, a panel or a figure of your own goes onto a cloned page, whatever the template drew in that space is still under it. clear_region(slide, box) empties the box -- every shape drawn in it, groups walked into -- where box is a Box.corners(...), a Box.at(x, y, w=, h=) or a page_box(shape), because the two corners a Box holds and the (left, top, width, height) a template reference prints are the same four numbers and a bare tuple of them is refused rather than guessed at -- and returns what it removed, with left_standing for anything that still lies over the box. Read that. The shape the box sits inside is kept on purpose: on a template that is the card the chart was drawn in, and the card is the arrangement the page was cloned for. keep=("the words a shape shows",) or keep=(7,) spares one, and share= (half by default) decides how much of a shape has to be inside the box to go.

Do not write the sweep yourself: a sweep keyed on a shape's language or size keeps the arrows, the number labels and every connector (a rule is 0.00in tall), and the chart goes on top of them. Only a region takes everything in it.

slide = clone_page(prs, prototype(tpl, 4))
replace_text(slide, "the template's title", "System prices fell 31% in one year")
print(clear_region(slide, Box.corners(0.95, 1.95, 12.55, 6.55)))   # say what went
column(slide, Box.corners(0.95, 2.30, 4.15, 6.05), T, prices, unit="$")

A shape's own box is a region, so clearing exactly where the template drew something is clear_region(slide, page_box(shape_at(slide, 3))) -- page_box answers in the two corners a region is given in, with the scale the shape's group applies to it.

Numbering: an integer is the shape's place on the page, as the read-back prints it (# [n], groups opened, counting shapes that cannot be drawn as well as those that can). shape_at(slide, n) counts the slide as it stands now, so it is the same numbering only while nothing has been removed -- drop_shape and remove_unit both renumber everything after them. Prefer replace_text keyed on words, which no deletion disturbs; where you do need an index, take the handle before you delete anything.

A cloned page keeps the template's words, so a line you did not replace is still saying them. That is this route's safety rather than a nuisance: the miss is visible on the page, and the build refuses to publish it and quotes the line back (placeholder_copy, §12). There is no third answer -- a block is replaced, or taken off the page with drop_shape. The one line that stays is a structural page's own label: the 目录 or Agenda a template writes on its index page is what that page is called, and the check does not ask for it back. The alternative was measured and rejected: a call that emptied what it was not told about turned the same mistake into a page holding a title over twenty blank boxes with every check green, and a blank page is a page nothing can name.

An emptied shape is not a removed one. Writing "" empties the text and leaves the box standing, because the box is often the design -- a tinted panel, a numbered circle. So "" is not a spelling of delete: written into agenda slots a deck has no sections for, it ships the numbered bubbles anyway. drop_shape(shape) takes a shape off the page -- reach it with shape_saying(slide, "the words it holds") or shape_at(slide, n); remove_unit(unit) takes a whole slot out and closes the row up. Reach for a shape by the words it holds or by the box you kept, not by counting to 7 again after something was removed.

Never lay a new text box over a page you cloned. The construction is clone_page for the background, add_textbox for the copy, replace_text never called — and it ships the template's own "单击此处添加长一点的副标题" under your own text on every page. placeholder_copy and template_underlay both refuse it: if you cloned a page, every word on it arrives by replacing a word that was there.

The template's photographs are placeholders. A cover's stock photograph of a meeting table says nothing about the deck's subject: replace it with a figure from the sources or take the frame out — replace_picture(shape_at(s, 7), f"{FIGURES}/fig3.png"), or drop_shape(shape_at(s, 9)). An image under a tenth of the page is different: an icon, a corner flourish or a rule, and part of the design.

A landscape figure does not go in a portrait frame. replace_picture places it anyway and warns when the two are more than 2x apart, saying both numbers and what the fit did -- contained it sits as a strip in an empty frame, cropped it loses about that share of the figure -- and the warning comes back in the build reply's warnings. A photograph cropped to a banner is a crop a designer makes; a chart that lost its outer columns is not, so look at the render and, where the figure matters, give the frame the box it needs: replace_picture(shape_at(slide, 4), f"{FIGURES}/fig2.png", box=(0.8, 1.6, 7.4, 4.2)), where the four numbers are (left, top, width, height) in inches — a size, the same one place takes, and not the two corners a ppt_layout.Box holds. A Box handed over whole is converted; what you must not do is unpack one into four numbers, because Box.corners(0.72, 1.24, 12.6, 6.7) read as a size draws a 12.6x6.7in frame off the side of a 13.33in page.

Some of a template's photographs are on its layouts, not its pages. Several bundled templates carry the cover's, the section page's and the closing page's picture on the layout, so every page built on it inherits the same picture and a replace_picture on the cloned page never reaches it -- template_picture cannot see it either; layout_picture names the layout, and ppt_template lists them as layout_pictures. layout_pictures(slide) returns those shapes, largest first, and replace_picture(layout_pictures(slide)[0], FIGURES / "cover.png", "cover") changes the picture for every page on that layout at once, which is what a house photograph should do. A picture the size of the page is the page's background: the template's own is a soft texture the type reads over, and a photograph swapped in at full strength drowns every title on that layout, so it takes the alpha. A picture generated in the deck's own style (§5) is the usual replacement; keep the template's own when it is the design -- an illustration the designer drew, not a stock photograph.

The pages you borrow

When no example of the bound template carries the page's information shape, borrow one from another bundled template before composing. ppt_template lists them under borrowable_pages, each by template, page and arrangement -- an S-curve of five pills, a ring of six labels around a hub, a photograph beside a numbered list. A borrowed page lands on this deck's own layout of the same name and its theme colours resolve to this deck's, so what comes across is the arrangement and nothing of the source's look.

from ppt_template import bundled, clone_page, prototype, replace_text

slide = clone_page(prs, prototype(bundled("gold_panel_year_end_summary"), 13))
for old, new in zip(read_back_keys, ["选址", "招商", "运营", "推广", "评估"]):
    replace_text(slide, old, new)

Record both on that page of the plan -- borrowed: "gold_panel_year_end_summary" and prototype: 13 -- so the checks that read the plan open the right file: which page the clone promised, whose placeholder copy and photographs to look for. bundled() takes the file name without .pptx and names the ones that ship when it is wrong. Only the content pages ppt_template listed are offered: the deck's cover, index and closing are always the bound template's own.

Everything above about a cloned page holds for a borrowed one -- replace_text per line, remove_unit for the slots the content does not fill, whatever you replace in neither still says the source template's example copy, and its photographs are placeholders.

Colours follow the deck; two things do not, and ppt_template names both. Every fill, line and run that refers to a theme colour resolves to this deck's theme the moment the page is cloned -- do not walk the page's shapes to recolour them, there is nothing to find, and on python-pptx merely reading shape.line.color puts an outline on every shape it touches. What does not follow: a picture is a bitmap, so an illustration painted in the source template's accents arrives in those accents -- replace_picture it out; and a label whose colour read on the source's card can land on a tint of this deck's that cannot show it -- ppt_template lists those pairs, and the build refuses the page under 2:1 as unreadable, naming the label. Answer that by writing the label in this deck's ink or filling its card darker, one line each, and leave the rest of the page alone.

The pages you compose

Adapt first, and compose what no example can carry. A page started from the nearest example by information shape arrives already in the house style -- the pitch of its rows, its title's own box, its accents, the spacing its designer chose -- and none of that is work you then have to do. Filling one leaves nothing behind either: remove_unit and fill delete the units your content does not fill, so a six-card prototype carrying four cards comes out as four cards. Compose where no example carries the page's information shape, and say which example you looked at in needs; a deck none of whose pages came from the template's own comes back as template_adherence (§12).

Start the page from the layout the template's own content pages sit on. That is layout_for_a_page_you_draw in the measured house_style, and add_slide on it brings the template's background with it — the corner device, the edge rules, the ground colour, none of which you have to draw, which is what makes a deck of composed pages read as one deck. add_slide on a blank layout, or painting your own background, is how a deck ends up with the template surviving as a colour.

The rest of house_style is the frame for what you then put on it:

layout_for_a_page_you_draw: "Title Only"     # add_slide on this, and the background comes with it
title_row: at (0.72, 0.14) 11.88x0.98in, 28pt, Arial, left-aligned, anchored bottom, on 7 of its pages
title_row_as_code: from ppt_layout import Box, write; TITLE_ROW = Box.corners(0.72, 0.14, 12.6, 1.12);
    write(slide, TITLE_ROW, claim, size=28, bold=True, align="left", anchor="bottom",
          colour=INK, font=FACE, cjk_font=HAN)
type_pt: {title: 28, subtitle: 24, body: 18, secondary: 16, caption: 14}
face: "Arial"
safe_area_corners_in: [0.72, 0.14, 12.6, 6.7]     # two corners, what Box takes -- not a size
body_area_corners_in: [0.72, 1.24, 12.6, 6.7]
body_area_as_code: from ppt_layout import Box; body = Box.corners(0.72, 1.24, 12.6, 6.7)
from ppt_layout import Box, write
T = THEMES[next(iter(THEMES))]                           # the template's own palette
layout = next(l for l in prs.slide_layouts if l.name == "Title Only")
slide = prs.slides.add_slide(layout)                     # the background comes with it
write(slide, Box.corners(0.72, 0.14, 12.60, 1.12), "消融:时序颈与语义查询是关键",
      size=28, bold=True, align="left", anchor="bottom",  # title_row_as_code, pasted
      colour=T["foreground"], font=FACE, cjk_font=HAN)
body = Box.corners(0.72, 1.24, 12.60, 6.70)              # body_area_as_code, pasted

and what goes in body is a structure out of deck/build/references/layouts.md with its modifier layers, chosen for what this page argues (§3.5).

Match the title row's anchor, not just its box. The box is where the row is and the anchor is where the ink lands in it, and the two readings of one 0.98in row are most of an inch apart: a master that says anchor="b" sinks a single-line title to the bottom of the row, while write anchors to the top unless told otherwise. title_row_as_code is that call with the anchor already in it, and a composed page whose anchor disagrees with the template's comes back as title_row (§12).

The subtitle row is reported the same way — and a template may set its own centred under a left-aligned title, which is its designer's decision and not a mistake to correct. Take the alignment from subtitle_row_as_code rather than assuming it matches the title's.

One scale for the whole deck. Every page title at type_pt["title"], body copy at type_pt["body"], captions at type_pt["caption"], the same role at the same size on every page (§3), and the floors of §3 under all three. type_drift (§12) measures it off the render.

Set every size yourself, and give the box the room it needs. write turns autofit off on purpose, so a box that cannot hold its copy comes back as a measurement rather than as type quietly dropping to 10.8pt. The answer to overset_copy is a taller box, a wider column, less copy or a second page — never a smaller size (§12).

Stay inside the safe area. safe_area_corners_in is where the template keeps its own content; a layout's artwork lives outside it, and copy laid across that artwork comes back as over_layout_art.

Ink is foreground. surface and background are what the ground is painted with. On a dark template those three are white, near-black and black, so reaching for "the dark one" as a type colour puts near-black type on black. One line is measured off the render, against the ground each box actually landed on: under 2:1 nothing is legible and unreadable refuses the deck (§12). Nothing is said above that line, so everything between "legible" and "comfortable" is yours to judge off the render (§10).

Take the face from the page, not out of the theme. ppt_theme's font_family is what the template's theme declares, and a template can declare 微软雅黑 while every run on its example pages is Arial. house_style's face is what the pages actually use — pass it as font= and its CJK companion as cjk_font=, or a line of mixed text comes out in two unrelated faces.

Which pages are cloned is decided in the outline (§2), not here. A page there that names no prototype is listed back to you as a question — pick the nearest content example, or say in needs why no example carries this page's information shape. A page that named one and was not built on it comes back after the build as prototype_kept, which reports rather than refuses (§12).

9. Formulas

Never set an expression with write, and never fake a fraction or a root. A fraction, a root, a sum with limits is TeX -- formula(slide, box, r"\frac{QK^{T}}{\sqrt{d_k}}", T) sets it as a typeset picture in the deck's ink; a slash and a √ character read as code. A formula in a text box is prose: it wraps where the box runs out, and where it runs out is the middle of a symbol — and every subscript in it is flat, so the one thing the notation carried is gone (flat_formula, §12). formula() sets it as one unbreakable line, steps down the ramp until it fits, and splits at the expression's own separators at the floor; formula_type_size says which step it will land on. deck/build/references/formulas.md has the notation it reads. Open it when a page carries mathematics.

10. Look at the deck

ppt_build renders a batch of pages back per call, each labelled with its number and anything measured on it, and says how many of the deck's pages it has not shown you. A build without slides shows the pages whose code changed since you last saw them -- the ones you just wrote or edited -- several at a time, and walks the deck from page_from= once every page has been shown; slides=[4, 5, 6] asks for exactly those pages instead. So looking at the deck is a few calls rather than one per page, and after an edit pass the plain build is the call to make: it comes back with what you changed. How many a call carries is configurable, and the tool schema names the numbers. A program that ran without error is not visual evidence. Open each render and name something concrete on it before you change it.

Reference — angles to look from, not a checklist and not a score. What the code measures is in §12; these are the readings nothing measures:

  • a role at the wrong step of the scale — a caption as large as the body, a title a point above its subhead, a source line as large as what it credits;
  • unreadable, distorted, blank or badly cropped figures;
  • empty charts, weak labels, bars out of proportion to their values, misleading scales;
  • icons too small to read, or crowding the text they belong to;
  • a sparse or mechanically forced layout — compartments where an argument belongs;
  • anything on §3's avoid list.

Two questions settle most of it: is the smallest type readable with the page shrunk to a third -- roughly the back of a lecture theatre -- and, beside the page before it, is this a different page or the same page with different words.

The reply names how many pages it did not show and how to ask for them: a named subset is not the deck (§11).

A page that raises no longer stops the deck. The build runs your # SLIDE n blocks one at a time: a block that raises loses only its own page -- a page saying what went wrong stands in its place -- and every other page is still drawn, measured and shown. The failure comes back as a page_failed refusal with the traceback, beside the findings on the pages that did draw, so fix it in the same edit pass as those rather than building once per crash. Keep the shape that makes this work: shared helpers in the prelude above the first block, one slide per block, nothing a later block needs defined inside an earlier one.

Fix everything a build reported before you build again, and send the edits and the build in one reply. A build's reply lists every finding on every page it showed; answer all of them -- one edit_file per page named -- and then ppt_build, all in the same reply: tool calls run in the order listed, so the edits and the build cost one turn where a reply per call costs two. One build per finding is the shape of a slow run.

Name the pages you changed when a page is wrong. ppt_build(slides=[7]) rebuilds and hands back page 7 alone: edit that page's block, look at it, fix it, look again. A sweep tells you twelve pages have something wrong; a short loop tells you what. Run the full build when a batch is settled; that is what runs the gates and publishes.

What has to clear is the refusals. Everything else you judge once. A refused build is not delivered, so §12's refusals get answered whichever way you read them. The warnings are reports and the renders are the judge: read each one on the page it names, fix what you agree with -- and what looks wrong to you whether or not anything measured it -- and leave a finding you have looked at and disagree with alone. Judge it once: the same warning on the next build is the same warning, and rebuilding a deck nothing refuses to chase it changes nothing.

Somebody else looks, and you do not have to ask: ppt_review. A build with nothing refusing it runs this by itself once five of its pages have not been read -- a draft counts, so the first reading arrives while the deck is still being written -- and the list comes back in that same reply under first_reading, with the pages it found the most on rendered beside their entries. It reads the pages nobody has read yet, and a page you rewrite after it was read is read again, so the build that delivers the deck reads whatever is outstanding. Call it yourself for a second reading once you have answered the first, or ppt_review(pages=[7, 8]) to read back the pages you have just changed. It renders every page and reads each one on an empty context -- one page, its planned claim, and the requirements -- and hands back a problem list: where a region carries nothing, where copy crowds a rim, what does not line up, which group of cards has no icon in it, a figure too small or squeezed or off-centre in its own region, a column much wider than anything in it, type a step off its role, and whether the page still carries the claim it was planned for. pages=[7, 8] reads just those; omit it for the deck. It reports which of the two kinds a blank region is, so the entry names the answer as well as the defect.

It refuses nothing, publishes nothing and edits no file. Every entry is yours to judge on the same terms as a warning: look at the page it names, fix what you agree with, leave what you have looked at and disagree with -- and say why.

Saying why is a call, not a sentence in your reply. Every entry goes into a ledger with an id (p14-a1b2c3), and every build reply lists what is still open under open_findings until it is answered one of two ways: a re-reading of that page no longer sees it, or you look at the page and dismiss it -- ppt_review(project, dismiss=[{"id": "p14-a1b2c3", "reason": "the illustration is the template's design"}]), which reads nothing and returns the list as it stands. An entry reported again on a later reading keeps its id and shows how many times it has been seen. A deck delivered with open entries is a deck whose reader was never answered, and the reply says so on every build.

One reason is refused: "it is the template's own illustration". The template's stock pictures and drawings are placeholders, and keeping the template's style means its layouts and colours, not a whiteboard meeting on a page about elderly care. A figure entry about one of them is answered by replacing the picture -- ppt_generate_image in the template's own style, or a real photograph from ppt_image_search -- or by saying what the picture depicts and why that is this page. A dismissal that only says whose picture it is comes back as refused, and the entry stays open.

One page at a time, and a verdict on a kind is not a judgement. The entries already carry their kind; what makes the list worth having is that each one names a page. "Most of these are whitespace on the table pages, and the cover art is the template's, so keeping them" answers eighteen pages without opening one. If you are going to leave an entry, open its page, say what you saw there, and name the page when you say it. Dismissing a kind is how a list of eighteen becomes a list of two without anything being read, and the ones a category sweeps away are the ones the reviewer could see and you could not.

Why a second reader rather than more looking. You wrote these pages, and a region you filled on purpose reads to you as a decision already made -- so a warning against it is one you have already answered. The reviewer has none of that: it did not choose the layout, and it is not told your program, only the plan -- so it sees the colour panel four times the height of its two lines, and the table that stops well short of the band its card row fills, which cleared every gate.

When the pages read right, stop. A build nothing refuses has delivered the deck, and after ppt_review there is no further pass, no stage that rearranges the pages once the gates clear. So a page you send on unlooked-at ships as it is, and a deck you have read through -- and had read back to you -- and would not change is finished.

Where the user named the file's destination, the build delivers there. State it once as ppt_build(deliver_to="/absolute/dir/name.pptx") -- or a directory ending in / to keep the deck's own name -- on any build, a draft included; it is kept for the deck, and every build that publishes writes out/ and copies the same bytes there, the first time and on every revision after. The reply comes back with delivered_to and the slide count, and those are the words to give the user. One file lands: the .pptx. The render under out/ is the engine's own preview and is not delivered or handed over. A copy you make with exec is recorded nowhere, is not the deliverable, and is called out as such.

11. The order is enforced, not suggested

ppt_build refuses until ppt_prepare has read the task, and until ppt_brief holds the three things the user decides. And it refuses a deck carrying a page whose code has never been rendered back to you: the record is keyed on the block that drew each page, so a page you looked at stays looked at and a page you then edited does not. A draft render counts -- it is a page put in front of you -- so a deck drafted through page by page has nothing left for the publishing build to refuse. slides=[1] on an eighteen-page deck is not a way to ship seventeen pages nobody saw.

That one cannot be answered by editing the deck. Run the build again, walk the rest of the deck a batch at a time with page_from, and look.

One more thing the order decides: deck/ingest/materials.md does not exist until ppt_ingest has run. Reaching for it first gets a No such file or directory. When it is there, ppt_ingest returns materials_index — every part of it with the line it starts at — and read_file takes offset and limit. Read the parts the deck stands on rather than the file: whatever you read is re-sent on every request after it, so a single whole-file read is the most expensive thing in the deck.

12. What the build refuses, and what it only reports

Refused, with the page named. Five land at the outline, before a line of the program is written:

Refused at the outline The move
a figure id the catalogue does not hold take the id from the catalogue
a layout id the catalogue does not carry -- P1 to P41 are page structures, M1 to M26 modifier layers, not zero-padded open deck/build/references/layouts.md; an id not in it can only come from not having opened it
with a template bound, an outline whose cover, index and closing do not name the template's own pages name them
when ingest extracted no figures at all, a cited page nobody opened web_fetch each URL the materials cite and take its image links, then ppt_fetch what you will use -- or the PDF behind an abstract, so ppt_ingest extracts its figures
with a template bound, a plan composing more than a quarter of its content pages from scratch -- neither a prototype nor a borrowed page give the rest the nearest example by information shape and adapt it; keep composing only the pages whose shape no example carries, and say which in needs
a borrowed naming no bundled template, or a page of one that is not a content page take the template and page from borrowable_pages in the ppt_template reply; the bound template's own pages take prototype alone
a plan starting more of its content pages on one prototype than on all the others together -- four pages at least, and only where this template's own unused examples can carry the repeats the reply names an example per repeating page by what that page says it carries; record prototype: N on those pages. Keep the one prototype for pages that are a series a reader compares across, and say in needs which shape no other example carries

For each URL that holds nothing usable, or will not load, say so in ppt_outline's swept: [{"url": "...", "found": "text only, no figures"}] -- kept with the deck, so a later call does not ask again. Nothing is recorded until all four clear.

The rest land on the built deck:

  • a page citing one figure while showing another;
  • a length the brief did not agree, or the wrong language;
  • a page the build cannot map back to a block of your code;
  • a page whose block raised while the build ran it on its own (§4): a page saying so stands in its place and the rest of the deck was still drawn, so fix that block in the same pass as the other findings;
  • a page you have never been shown;
  • a page whose plan promised a figure and that shows no picture -- place it, or plan the page again without it;
  • a page whose plan planned a table and that shows none -- draw it with ppt_layout.table(), or plan the page again without one;
  • type a reader cannot make out: under 2:1 against the ground it landed on;
  • words colliding in the render, which is not answerable by shrinking them;
  • copy set above where its box starts: a frame anchored to the middle or the bottom of its box holds its last line on the box's bottom edge, so a line the box cannot hold is added above the first one -- off the top of the page, or over what the page already drew there. Anchor the frame to the top, or place() it: the box, the copy and every neighbour stay as they are. Nothing here asks you to cut a line, and a frame that shrinks its own type to fit is not in this at all;
  • the template's own placeholder text still on a page you cloned, or that page cloned for its background with new text boxes laid over it;
  • a page you cloned and never wrote into: under the copy its kind of page needs, and holding more empty text frames than frames with words in them. A frame written with "" is emptied, not removed, and a page of them is a page that says nothing -- write every line of the page with replace_text, and drop_shape or remove_unit what the page does not use.

Reported, with a page number, and every one of them yours to judge:

Reported What it means The move
band a filled colour bar carrying nothing: a full-width band -- 85% of the page or more, under 1.3in tall -- with no copy on it, or a narrow accent strip -- 0.22in or less on its short side, six times that on its long -- wherever it sits and whatever sits on it put the copy on the band, or take the strip out. Under 4.5pt it is a hairline and nothing here looks at it. Bars encoding values are left alone, but only where the row reads as data off its geometry alone: two or more slots, and lengths that differ
type_floor type under the floors of §3 a taller box, a wider column, less copy or a second page -- never a smaller size
type_scale body copy over the 14pt floor but under BODY_PT, at a size not a step of the ramp size=BODY_PT, size=LABEL_PT, or the_largest_step_this_copy_takes(text, box, font=F)
type_drift one slot the deck sets at several sizes give the boxes the height one size needs
row_type_drift one row whose cards came out at different sizes: the copies of one slot inside a single repeating unit, measured against each other rather than against the size the slot was drawn at. Every box is behaving -- each shrank its own copy to fit -- and the row is what reads wrong, so no per-box reading can see it even out how much the units hold, or set that slot one size explicitly. Giving one card more height fixes that card and leaves the row uneven
outranked_title a page whose title and the line under it came out at one size, with the line under it the longer of the two, so a reader meets the caption as the heading. On a cloned page the title states no size at all -- it resolves through the layout and the master -- so the number your caption matched is one nothing in your program shows you; the finding names it state the caption a step down (size=BODY_PT, or size=LABEL_PT), or shorten it until it reads as a caption. Where the caption box is the template's own, its size came with the page rather than from your program: state a smaller one there and leave the title alone
evidence a deck with too few content pages showing anything -- a figure, a table, a chart or a diagram, counted across the deck rather than page by page plane is a filled shape like any other, so three cards come to three and the same three on a plane come to four
wide_table a table too wide to read fewer columns, or the page turned over to it
native_table a table still wearing Office's own look ppt_layout.table(), which takes the banding and gallery style off and keeps every row -- or draw the grid yourself, which never had them
rule_strike a rule struck through a row rule sits below the box it underlines; place it off the row
card_overflow copy escaping a card fits(body, card_body_box(box, icon=, title=)) first, then the height it asks for
crowded_panel a line crowding its panel's bottom rim the padding your other panels have, or one line less
covered_shape content three fifths hidden behind an opaque shape drawn after it, read off the file's z-order rather than off the render draw the covering shape first, or shrink it off what it hides. It reads the file and not the page, so a shape that covers nothing a reader was looking at is yours to dismiss -- the render-side check that can prove a reader lost something is word_collision, and that one refuses
off_page a shape over the page edge inside safe_area_corners_in
house_style a theme that is not the bound template's, read off the theme's colour scheme slot by slot open the deck with Presentation(os.environ['PPT_TEMPLATE']) rather than Presentation(). If you repainted the theme deliberately, this is the finding to leave alone -- ppt_template(repaint=true) is the way to do it that keeps the two in step
literal_escape copy printing an escape as characters (48.3\nOVIS, &nbsp;, _x000B_) pass a real line break -- a newline in the string, which becomes a new paragraph -- or split the copy into two values. A paragraph carrying a quote or a bracket is read as showing code and is left alone
boxless_copy copy in a box with no height, or no width, so the page shows nothing of it however much it says give the box the size its copy needs (text_size, card_size), or take the block off the page. Read off the declared geometry, so a box your own code sized to zero is the case it was written for
spilled_copy copy painted off the page by a wrap=False box turn wrapping on and give the box a second line's height
over_layout_art copy on the layout's artwork inside safe_area_corners_in
flat_formula an expression set as prose, so its subscripts are flat and the box may break it inside a symbol formula() (§9)
listed_claims one box holding two or more parallel claims, so they read as a list to be read out separate them so a reader can see where one ends; §6.5 has the calls that measure a region before it is drawn
orphan_line a label the render broke onto a second line, as "为什么要统 / 一", in a text box or a table cell a hair more width, or fewer characters
wrapped_label a label wrapped in a box too narrow for it the width text_size says the line takes
overset_copy copy that does not fit the box it was put in the taller box it names; text_size, card_size and points_size say the same number before the build does
clipped_copy copy the file states that the render clips instead of wrapping turn wrapping on
excessive_whitespace a large blank field between body groups, below the content, or inside a panel grow the load-bearing content, redistribute it, or shorten the panel
unseparated_blocks two groups with no more air between them than inside them a wider gap, a surface, or a hairline
undivided_body a body of three or more blocks of copy with nothing drawn between them -- no panel, no rule, no outline, no figure. Its sibling above is about the air between two groups; this one is about whether the division was drawn at all one device is the whole floor: card_group for blocks of one kind, plane behind the region that answers the page, rule between two halves that are not cards, or a figure the copy is set against (§3.5)
title_row titles that start at different left edges, or sit at a different anchor inside that row than the template's own title_row_as_code, decided once in the shared setup (§8)
layout_variety composed pages that nearly all resolve to one page structure, read off the shapes each page actually carries a structure chosen for what each page argues (§3.5)
page_mapping a build whose pages cannot be told apart in what the run recorded -- a separate reading from the refusal above, which is about the # SLIDE blocks in your file where this one is about what the run made of them. The two can arrive together one block per page, each composing its own page

| template_adherence | a deck none of whose pages came from the template's own structural pages | clone the cover, the contents list, the divider and the closing (§8) | | template_picture | the template's photographs still showing | replace_picture(shape_at(s, n), ...) a figure in, or drop_shape | | placeholder_marks | the template's numerals and marks left on a page -- its glyphs, its lone tokens -- said once per page with a few named, where a phrase of the template's refuses on its own | keep the ones the design draws, replace_text or drop_shape the rest | | prototype_kept | a page built on a prototype other than the one its outline named | either page reads, so build it or rewrite that outline line |

At the plan, four more: a page planned with one line and nothing else, a deck spending a quarter of itself on pages carrying no argument, material too thin for the pages agreed, and a page drawn from scratch where the plan named no prototype -- that last one arrives as a question, to answer with an example page or a reason in needs. At ingest, a source that could not be read at all.

Warnings are mostly not refusals for a reason. Nearly every one of them can be "fixed" by making the type smaller, and a gate that demanded they clear would get exactly that. The two that do refuse -- words over words, and content behind a panel -- are the two that shrinking makes worse.

Three silences the build reports rather than hides. With nothing ingested there is no figure catalogue, so a page crediting Figure 4 while showing Figure 5 would not have been caught. With no brief recorded, neither the deck's length nor its language has been checked against anything agreed. With no renderer on the machine, nothing measured on the rendered page ran at all. None of the three stops a deck, all three mean "not checked" rather than "clean", and all three belong in what you tell the user.

What nothing checks at all: whether a number or a name on a page is one the sources printed. There is no index of what the materials state and no gate that reads one, so every figure on every page is yours to have read correctly and yours to be right about.

Version History

  • f4317bd Current 2026-10-18 15:50

Same Skill Collection

demos/skill_retrieval/skills/image-gen/SKILL.md
plugins-dist/design-engine/raven_design/skills/build-content-websites/SKILL.md
plugins-dist/design-engine/raven_design/skills/build-games-and-playful-experiences/SKILL.md
plugins-dist/design-engine/raven_design/skills/build-interactive-explainers/SKILL.md
plugins-dist/design-engine/raven_design/skills/build-polished-visual-frontends/SKILL.md
plugins-dist/design-engine/raven_design/skills/build-product-and-tool-interfaces/SKILL.md
plugins-dist/design-engine/raven_design/skills/build-ui-components-and-systems/SKILL.md
plugins-dist/design-engine/raven_design/skills/create-data-visualizations/SKILL.md
plugins-dist/design-engine/raven_design/skills/create-illustrations-and-scenes/SKILL.md
plugins-dist/design-engine/raven_design/skills/create-maps-and-spatial-plans/SKILL.md
plugins-dist/design-engine/raven_design/skills/create-technical-diagrams/SKILL.md
plugins-dist/design-engine/raven_design/skills/design-brand-identities/SKILL.md
plugins-dist/design-engine/raven_design/skills/design-editorial-and-presentations/SKILL.md
plugins-dist/design-engine/raven_design/skills/design-icons-and-symbols/SKILL.md
plugins-dist/design-engine/raven_design/skills/design-typefaces-and-lettering/SKILL.md
plugins-dist/design-engine/raven_design/skills/review-against-ai-patterns/SKILL.md
plugins-dist/design-engine/raven_design/skills/visual-artifact-design/SKILL.md
raven/memory_engine/skills/raven-self-config/SKILL.md
raven/memory_engine/skills/weather/SKILL.md
agents/raven-design/skills/deck-to-pptx/SKILL.md
plugins-dist/design-engine/raven_design/skills/create-marketing-graphics/SKILL.md
skills/git-story-film/SKILL.md

Metadata

Files
0
Version
f4317bd
Hash
b62fb56b
Indexed
2026-10-18 15:50

Главная - Вики-сайт
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-19 05:41
浙ICP备14020137号-1