md2star.pptx_assemble module

Seam-D PPTX assembler: build a deck directly on the AI-selected layouts.

md2star.pptx_layout decides, per slide, which of a designer template’s named layouts best carries that slide’s content. This module is what actually uses that decision: for every slide it instantiates the chosen layout (not a fixed handful of curated backgrounds) from the real template and overlays guaranteed-legible content, so the deck’s visual variety follows the AI’s per-slide reasoning instead of a hand-picked round-robin.

Design choice, carried over from the PoC that proved it out: fighting a designer layout’s own placeholders is fragile (their colours and geometry vary wildly, some are dark-on-dark, some clutter a small box next to a big void). Instead each slide keeps the chosen layout’s background (its true branding) and places our own guaranteed-safe text/image boxes in it, sized from the layout’s actual placeholder geometry when the catalog recorded one (title / body / picture), and a sensible fractional default otherwise — so it degrades gracefully for template layouts the catalog under-described.

With one exception, reconciled rather than overlaid: the title goes into the layout’s own title placeholder when it has one. A slide’s outline entry and its screen-reader reading order come from placeholders, not from loose text boxes, so a deck built purely of overlays had no titles at all where it counts — and the empty placeholder left underneath each overlay was a “Click to add title” ghost and a guaranteed overlap. Body text keeps its overlay (a designer body placeholder brings its master’s own bullet glyphs, which would double up with the assembler’s), and every placeholder still empty once a slide is finished is removed. See the “placeholder / overlay reconciliation” section.

Text colour is not hard-coded white: with an arbitrary designer layout now in play (not just a curated dark subset), a slide’s background can be light, so the assembler reads the layout’s own background colour and switches to a dark foreground when needed. This is the correctness gap the curated-background PoC never had to close.

A layout is not the whole template, though, and reference_slides=True opts each slide into Seam E (md2star.pptx_clone) first: where the chosen layout has an example page in the template that can carry this slide’s content, that page is cloned and its text replaced, so the slide inherits the designer’s decorative shapes and typography and not just their background. It is strictly an addition — every refusal falls through to the Seam-D path above, which can place anything.

The target-matching Ralph Eyeball Loop (eyeball_slides()) closes the verification gap the same way md2star.reverse_diagrams does for diagrams: render the assembled slide, show it and the chosen layout’s own catalog thumbnail to a vision model, and apply bounded, spec-level fixes (shrink text, trim bullets, grow an undersized image) rather than ever hand- editing the picture. Its model-free counterpart is md2star.pptx_audit, which measures the saved deck for the defects arithmetic can settle on its own.

Author

[Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui/)

class md2star.pptx_assemble.AssembleResult(pptx_path: 'Path', placed: 'int', total: 'int', cloned: 'int' = 0, pages: 'dict[int, int]'=<factory>, unjudged: 'list[int]' = <factory>)[source]

Bases: object

Parameters:
cloned: int = 0

How many of the placed slides were carried by Seam E — cloned from one of the designer’s own example pages rather than built on a bare layout. Always 0 unless reference_slides=True, and never all of them: a page that cannot carry its content is refused, not forced.

pages: dict[int, int]

Slide index -> the example page that carried it, for the slides Seam E took. Reported rather than written back onto the Slide models, so assembly stays a pure function of them; it is what lets the eyeball loop judge a cloned slide against the page it was cloned from and move it to another one. See eyeball_slides().

placed: int
pptx_path: Path
total: int
unjudged: list[int]

Slides the eyeball loop’s vision judge gave no verdict on (timeout, empty or unparseable reply) on its last pass. Their verdict fell back to the measured audit alone; listed so “no complaint” is never mistaken for “approved”.

md2star.pptx_assemble.CREDIT_NAME = 'md2star-credit'

Name every photo credit carries, so md2star.pptx_audit knows the small type over a picture is deliberate (a credit, like a footer).

md2star.pptx_assemble.CREDIT_SIZES = (10.0, 9.0)

a long URL comes down to 9 pt, then breaks after a « / » onto a second line rather than being cut.

Type:

Credit sizes tried in turn, largest first

class md2star.pptx_assemble.Item(text, level=0, bullet=True)[source]

Bases: object

One paragraph of slide text: its words, list depth, and whether it is a bullet.

Parameters:
bullet: bool = True
level: int = 0
text: str
md2star.pptx_assemble.LATENT_KINDS = frozenset({'dt', 'ftr', 'sldNum'})

PowerPoint fills the date, the footer and the slide number itself, from field codes, at display time. python-pptx already declines to clone these (“latent” placeholders), so in practice they never reach a slide this module built — the guard is here so that deleting “every empty placeholder” can never mean deleting a deck’s page numbers, whatever path the presentation arrived by.

Type:

Placeholders whose emptiness is correct

md2star.pptx_assemble.PHOTO_TITLE_WORDS = 6

the band holds one line.

Type:

Words a title over a photograph should not exceed

md2star.pptx_assemble.SMALLEST_BASE_PT = 16

The smallest type the assembler sets on a content slide — its bullets. A font_delta is one number applied to every size on the slide, so this is the one that binds: a delta computed from an overrunning 26 pt title would take 16 pt bullets to 8 pt, which the audit then reports as illegible (observed, on a real deck, from the first version of the exact correction).

md2star.pptx_assemble.TITLE_KINDS = {'ctrTitle', 'title'}

Placeholder kinds that are a slide heading. Public because Seam E (md2star.pptx_clone) classifies a designer page with the same vocabulary the assembler writes with.

md2star.pptx_assemble.add_code(slide, code, box, *, font, size=13, shrink=True)[source]

A dark rounded panel holding code, every line of it.

It used to keep the first twelve lines and drop the rest unannounced; a long listing now shrinks to fit (or, under --min-pt, overruns and is reported by the audit) instead of being cut.

Parameters:
Return type:

None

md2star.pptx_assemble.add_credit(slide, picture, text, *, family=None)[source]

Print text small in a corner of picture, on a dark translucent plate.

Bottom right inside the picture’s frame, a small margin in; top right when that corner would cover the title, a caption or a band the designer drew (a full-bleed photo’s title band sits at the bottom). White on a 60 % black plate reads on any photograph. The shape is named CREDIT_NAME: the audit exempts it from the type-size floors and from the picture-over-text overlap, both of which are its very design. Returns the text box, or None when there is nothing to credit.

Parameters:
  • text (str)

  • family (str | None)

md2star.pptx_assemble.add_image(slide, img, *, box, alt='', credit='')[source]

Contain-fit img in box (inches); credit goes over it (add_credit()).

Parameters:
md2star.pptx_assemble.add_table(slide, rows, box, *, size, font)[source]

A real PowerPoint table for a Markdown pipe table.

Tables used to vanish from smart-layout slides: the text parser skipped every | row and no branch placed them. The template’s default table style supplies colours; only the size (and the face) is set here.

Parameters:
Return type:

None

md2star.pptx_assemble.add_text(slide, blocks, box=None, *, color, center=False, middle=False, into=None, shrink=True, levels=None, plain=None, face=None)[source]

Overlay a text box. blocks = list of (text, pt_size, bold, font).

With into set to an existing shape (a cloned placeholder), writes into that shape instead of adding a new one, and ignores box — the shape’s own geometry is the designer’s and is already what box was derived from.

A None size, weight, face or color is not written, so the run inherits it from the template (see declared_style()): that is how --min-pt keeps a designer’s 30 pt body instead of overwriting it. shrink allows the renderer to shrink text to fit its shape; off under --min-pt, where shrinking is exactly what the floor forbids. levels sets each paragraph’s list depth and plain marks paragraphs that must not get the placeholder’s bullet glyph (prose, numbered items).

Every paragraph’s lines are broken by md2star.linebreak when the box’s width and the text’s size are known: never after « de » or « the », never before a lone « : », never between a number and its unit. face is the family measured when a block inherits its font (None: 0.55 em).

Parameters:
Return type:

None

md2star.pptx_assemble.assemble(slides, catalog, template, out_path, *, image_base_dir=None, render_mermaid=None, default_layout=None, reference_slides=False, avoid_pages=None, min_pt=None)[source]

Build a real .pptx directly on each slide’s AI-selected layout.

For every slide, instantiate slides[i].chosen_layout from template (falling back to default_layout, or the template’s first layout, when the pick is missing or unknown) and overlay title/body/image/code content sized from that layout’s own placeholder geometry when the catalog recorded one. Per-slide overrides (written by eyeball_slides()) bound font size and image growth. Every paragraph, table and code line of the slide is placed: nothing is trimmed to make it fit.

With reference_slides set, each slide is first offered to Seam E (md2star.pptx_clone): when the chosen layout has an example page in the template that can carry this slide’s content, that page is cloned and its text replaced, so the deck inherits the designer’s decorative shapes and typography instead of only their background. A refusal (no suitable example, a chart on the page, fewer bullets than columns) falls straight through to the Seam-D path below, which can place anything. avoid_pages maps a slide index to example pages it may not use — what eyeball_slides() writes after a vision model has judged one of them not to be carrying that slide.

With min_pt (the CLI’s --min-pt), no slide text is set under that size: body text goes into the layout’s own body placeholders, keeping the size, colour, face and bullets the designer declared whenever they clear the floor; overlay text uses the declared size or the floor; and nothing is allowed to auto-shrink. Overflow is then the audit’s to report and the eyeball loop’s to fix by moving or splitting the slide.

Returns:

The output path plus how many of the slides were placed without error (a per-slide exception is logged and skipped, never aborts the deck), and how many of those a cloned example page carried.

Return type:

AssembleResult

Parameters:
md2star.pptx_assemble.background_rgb(layout)[source]

The background a slide on layout shows, when it is one colour.

The layout’s own <p:bg> first, then its master’s — the step that was missing: a template whose dark background is declared once on the master (the common case) used to read as “unresolvable”, hence light, hence near-black text on a dark slide. A picture background is unknowable and returns None; a gradient is judged by its first stop.

Return type:

RGBColor | None

md2star.pptx_assemble.body_items(body)[source]

Every paragraph of visible text on a slide, in order, nothing capped.

Headings, tables, fences, images and div markers are not text paragraphs (each has its own branch in assemble()). List items keep their depth (two spaces or a tab per level); a numbered item keeps its number and gets no bullet glyph; wrapped lines join the paragraph they continue.

Parameters:

body (str)

Return type:

list[Item]

md2star.pptx_assemble.clear_slides(prs)[source]

Drop every slide the template ships with, keeping layouts/masters.

Parameters:

prs (Presentation)

Return type:

None

md2star.pptx_assemble.content_box(prs, info)[source]
Parameters:
Return type:

tuple[float, float, float, float]

md2star.pptx_assemble.credit_lines(text, max_w_pt, family)[source]

The size and lines a credit is set in, inside max_w_pt points.

One line at the largest of CREDIT_SIZES that fits; failing that, two lines at the smallest, broken after the « / » that balances them best. It measures and breaks whatever text it is given. The address is cut to its site by short_credit() before this is called; the full address survives as the link, so the printed credit can stay short.

Parameters:
Return type:

tuple[float, list[str]]

md2star.pptx_assemble.credit_pictures(pptx_path, markdown)[source]

Credit the pictures of a deck Pandoc wrote; returns how many were credited.

Pandoc’s PPTX writer puts an image’s title into the picture’s descr (its alt text, followed by the image’s path) and prints nothing. Every picture whose descr is one of markdown’s image titles gets add_credit(), and its descr goes back to the authored alt text: the credit is printed, not read out as the picture’s description.

Parameters:
Return type:

int

md2star.pptx_assemble.declared_style(shape, level=0)[source]

The point size and colour shape inherits at level, from the template.

Returns:

The inherited size (None when nothing on the chain declares one), its colour when resolvable to RGB, and whether a colour is declared at all (a theme colour the reader cannot resolve still counts: the designer chose it, so the assembler should not paint over it).

Return type:

tuple of (float or None, RGBColor or None, bool)

Parameters:

level (int)

md2star.pptx_assemble.drop_empty_placeholders(slide)[source]

Delete every placeholder this slide inherited but never filled.

Returns:

How many were removed — reported by the caller’s debug log, and the invariant md2star.pptx_audit.audit() re-checks on the saved deck.

Return type:

int

md2star.pptx_assemble.eyeball_slides(slides, catalog, template, out_path, *, indices=None, max_iterations=1, chat=None, render_template=None, render_deck=None, image_base_dir=None, render_mermaid=None, default_layout=None, allow_reselect=True, reference_slides=False, min_pt=None)[source]

Target-matching Ralph Eyeball Loop over an assembled deck.

Assembles (or re-uses an already-assembled) deck, renders it, and for each of indices (default: every slide) shows a vision model both the candidate render and the chosen layout’s own catalog thumbnail. On a mismatch it writes a bounded overrides entry back onto the slide (shrink text, cap bullets, grow an undersized image — never touches the picture itself) and re-assembles, up to max_iterations passes. Any transport/render failure leaves the current best assembly untouched.

With reference_slides on, the loop gets sharper on both ends. Its target for a cloned slide is the designer page that slide was cloned from rather than the layout’s representative thumbnail — the tightest comparison available, since the candidate is literally that page with the deck’s own content in it. And it gains a fix it did not have: when a cloned slide is flagged and no bounded override applies, it moves the slide to a different example page of the same layout before considering a layout swap. A designer who demonstrated one layout five ways has given five compositions to try, and reaching for another of theirs is a smaller, truer correction than abandoning the layout they chose. If none of the remaining pages can carry the slide, assembly falls back to Seam D on its own, which is the safe generic this loop has always wanted.

A judge that times out or answers nothing is unknown, not “matches”: the slide is judged on the measured audit alone and listed in AssembleResult.unjudged.

With min_pt, an overflow is never fixed by shrinking: the slide moves to the next layout that holds it at the floor, and when none is left it is split in two (continuation titled “(suite)” / “(cont.)”).

render_deck renders the assembled output deck itself (default: md2star.pptx_layout.render_deck_pages(), LibreOffice + poppler); injectable so the loop is testable offline without either installed.

Parameters:
Return type:

AssembleResult

md2star.pptx_assemble.fallback_bullets(body, cap=None)[source]

The slide’s text paragraphs, markup stripped — all of them unless cap.

There used to be a default cap of four bullets of ten words each, applied silently. That was content loss presented as layout; the full text now goes on the slide, and a slide too long for any layout is split into continuation slides upstream (md2star.pptx_layout.paginate()).

Parameters:
Return type:

list[str]

md2star.pptx_assemble.image_box(prs, info)[source]
Parameters:
Return type:

tuple[float, float, float, float]

md2star.pptx_assemble.image_title(target)[source]

The Pandoc title of one image target (path "title"), or "".

Parameters:

target (str)

Return type:

str

md2star.pptx_assemble.is_content(features)[source]
Parameters:

features (list[str])

Return type:

bool

md2star.pptx_assemble.is_dark_layout(layout)[source]

True/False for a resolvable one-colour background, else None.

Resolved through background_rgb(), so a dark background declared on the master (and only inherited by the layout) counts.

Return type:

bool | None

md2star.pptx_assemble.is_ornament(text)[source]

Whether text is ornament (or nothing) rather than someone’s words.

Parameters:

text (str)

Return type:

bool

md2star.pptx_assemble.layout_index(prs)[source]

Map every distinct layout name to its SlideLayout object.

Parameters:

prs (Presentation)

Return type:

dict[str, object]

md2star.pptx_assemble.notes_paragraphs(notes)[source]

Speaker notes as (text, level) paragraphs, list structure kept.

The notes pane is read mid-talk, at a glance: one paragraph per bullet, with the bullet drawn as “•” and nested items indented, reads the way the author wrote it — a single blob, or raw “- “ markup, does not.

Parameters:

notes (str)

Return type:

list[tuple[str, int]]

md2star.pptx_assemble.parse_alt_text(body)[source]

The first image’s Markdown alt text ("" when it has none).

Authored alt text is the accessible description of the picture, and dropping it makes the deck unreadable to a screen reader. Pandoc’s own PPTX writer carries it into the shape’s descr attribute; this is how the assembler keeps parity with the path it bypasses. md2star can even write this text for you — see md2star.preprocessing.alt_text.

Parameters:

body (str)

Return type:

str

md2star.pptx_assemble.parse_code(body)[source]

First non-Mermaid fenced block (the slide’s primary code sample, if any).

Failing a fence, a Pandoc line block whose every line is inline code (``| git push ``) is a listing too: one source line per line.

Parameters:

body (str)

Return type:

str | None

md2star.pptx_assemble.parse_image(body, base_dir)[source]
Parameters:
  • body (str)

  • base_dir (Path | None)

Return type:

Path | None

md2star.pptx_assemble.parse_image_credit(body)[source]

The first image’s Markdown title: its photo credit ("" when none).

![](photo.jpg "Courtesy of https://...") puts the credit where Pandoc keeps an image title. It is printed small over the picture (add_credit()), never used as the picture’s description.

Parameters:

body (str)

Return type:

str

md2star.pptx_assemble.parse_mermaid(body)[source]
Parameters:

body (str)

Return type:

str | None

md2star.pptx_assemble.parse_table(body)[source]

The first Markdown pipe table on the slide, as rows of cell text.

Parameters:

body (str)

Return type:

list[list[str]]

md2star.pptx_assemble.placeholder_kind(shape)[source]

OOXML placeholder type of shape ("" when it is not one).

Read off the XML rather than python-pptx’s PP_PLACEHOLDER enum so the vocabulary is identical to the catalog extractor’s (md2star.pptx_layout._placeholder_kind()) with no enum-name mapping table to keep in sync. A <p:ph> with no type is OOXML’s generic content placeholder, which pandoc and PowerPoint both call obj.

Return type:

str

md2star.pptx_assemble.short_credit(text)[source]

The words a credit prints, and the full address it links to.

Courtesy of https://www.nga.gov/artworks/195605-gallery-drouot prints Courtesy of nga.gov: the site, not the whole path, which says nothing a reader can use from a screen. The full address is kept as the link, on the credit and on the picture itself, so a click still lands on the source.

Parameters:

text (str)

Return type:

tuple[str, str | None]

md2star.pptx_assemble.slide_placeholder(slide, kinds)[source]

The slide’s own cloned placeholder of one of kinds, or None.

Parameters:

kinds (set[str])

md2star.pptx_assemble.template_fonts(template)[source]

Derive the template’s own DESIGN fonts: {family, display, body}.

Reads every explicit typeface="..." on slide/layout shapes (not the theme, which carries Office’s long CJK fallback list) and picks the most- common non-system face as body, its heaviest same-family weight as display (titles). Falls back to Arial when the template declares no explicit design font. Read-only: unlike the PoC this is derived from, this never downloads or installs fonts — a missing font degrades to whatever substitute PowerPoint/LibreOffice picks, exactly like every other best-effort AI/rendering feature in this package.

Parameters:

template (Path)

Return type:

dict[str, str]

md2star.pptx_assemble.title_box(prs, info, *, divider=False)[source]
Parameters:
Return type:

tuple[float, float, float, float]