AI Engineering
Deck-as-Code: How a Fleet of Review Agents Saved My Conference Deck
Iterating a 24-slide conference deck under review pressure, I stopped editing PowerPoint by hand. Every version is now rebuilt by an idempotent python-pptx script from the last validated file, and reviewed by four parallel critique agents reading rendered frames — because the XML lies about what the eye sees. Eight blocking defects on a version I was ready to ship, one vertical-tab pitfall, and a ghost footer hiding in the slide master: here is the pipeline.
· 10 MIN READ
A few weeks before a conference talk, I had a 24-slide deck under active review — a reviewer pass every few days, arbitrations landing between passes, eight versions in a week, four of them in a single day. And I was iterating it the way most people iterate decks: open PowerPoint, fix the thing, save.
That workflow has two failure modes, and I hit both.
The first is divergence. A manual touch-up session forks the file: early in the cycle I ended up with two candidate “latest” files and no reliable way to tell which edits were blessed. Reconstructing the truth cost me an afternoon, and the lesson stuck: a hand-edited deck has no source of truth.
The second is worse: after the tenth pass over the same 24 slides, the naked eye stops seeing defects. A footer collision, a low-contrast bullet, a leftover review annotation — they become part of the furniture. I shipped a version I genuinely believed was clean. A fresh set of eyes found eight blocking visual defects in it.
The fix: stop treating the deck as a document and treat it as a build artifact. The validated .pptx became read-only source, every new version the output of a Python script, and review a fleet of four parallel critique agents reading rendered frames — not the file, the pixels. This post is that tooling, with the pitfalls that almost beat it.
Rule 1: the validated file is read-only
The core discipline is a single line: every build starts by copying the last human-validated file, and no script — ever — edits that file in place.
import shutil
from pptx import Presentation
SRC = "deck_validated.pptx" # the reviewed reference — never edited in place
DST = "deck_v9.pptx" # the build output — always regenerated
shutil.copyfile(SRC, DST) # every run starts from the validated bytes
prs = Presentation(DST)
apply_review_arbitrations(prs) # merges, deletions, re-ordering
apply_visual_fixes(prs) # footer safe-zone, contrast, placements
strip_reviewer_annotations(prs) # the markup rectangles from the review pass
prs.save(DST)
The important word is reconstruction, not patching. My first instinct was a patch script per review round: open the previous output, apply this round’s fixes, save. That accumulates assumptions — patch N silently depends on the exact state patch N-1 left behind, and three rounds in you are debugging the interaction between patches instead of the deck.
A total rebuild is idempotent by construction. Run it twice, you get the same output. Add a fix, re-run, and every earlier fix is reapplied from a clean state. When a review pass turned the 31-slide validated file into a 24-slide talk — five slides merged into one, three deleted, two re-ordered — all of those decisions live in the script as named transforms. The review’s outcome is code: diffable, replayable, and impossible to half-apply.
The payoff: under review pressure you can iterate aggressively, because no experiment can break the validated version. The worst outcome is deleting the output file and re-running the build.
Rule 2: the XML lies about what the eye sees
My first quality gates were structural: parse the .pptx with python-pptx, assert on the XML. Text present, shapes positioned, colors set. Those checks stayed green while the rendered deck was, in the fleet’s later words, not projectable. Three concrete ways the XML deceived me:
Inherited properties are invisible. A run whose color comes from the layout or master reports no color at the run level — run.font.color.rgb just isn’t there. My contrast checker found nothing wrong with a slide that rendered as saturated red on dark violet, a combination that fails any threshold you pick. The defect existed only in the composition of slide, layout, and master.
Some shapes exist on no slide. One version rendered a duplicated copyright footer on more than a dozen slides; grepping every slide’s XML found zero occurrences. The ghost lived in a slide master, inherited by every slide on that master’s layouts — and the obvious fix, deduping footers everywhere, would have deleted the only legal footer on slides driven by the deck’s other masters. Master-level defects need master-level surgery, and you will not find them reading slides.
Geometry is not rendering. Two shapes whose bounding boxes don’t overlap in EMU coordinates can still collide visually once text autofits, wraps, or overflows its frame. A timeline that cleared the footer band by the numbers had its date labels truncated and its baseline sitting on the page number at render time.
So the pipeline renders before it judges. LibreOffice headless converts the build output to PDF, and pdftoppm rasterizes one PNG per slide:
/Applications/LibreOffice.app/Contents/MacOS/soffice \
--headless --convert-to pdf --outdir frames/ deck_v9.pptx
pdftoppm -png -r 150 frames/deck_v9.pdf frames/slide
# frames/slide-01.png ... frames/slide-24.png
150 dpi is the sweet spot: high enough to judge contrast and collisions, small enough for a vision model to inspect every frame. One caveat: some antialiasing artifacts on light fonts only resolve at 300 dpi — re-render a suspicious slide at 300 before “fixing” a file that isn’t broken.
The fleet: four critics who have never seen the deck
With frames on disk, review becomes a fan-out problem. I dispatched four critique agents in parallel — vision-capable LLM sub-agents, each with a scoped brief and no memory of building the deck. Two briefs covered visual defects on the rendered frames (collisions, contrast, cropping, alignment, leftovers); two covered structure (narrative order, spoken transitions versus actual slide order, hidden slides, duplicated closings). Together they produced seven reports, merged into one severity-ranked defect list.
On the version I believed was clean, the fleet found eight blockers at the top of the severity scale. Neutralized examples, because the defects are the point, not the content:
- Slide 3’s timeline collided with the footer band, and its date labels were truncated mid-word.
- Slide 10’s title overlapped its subtitle, and the bottom row of cards was cut by the footer.
- Slide 11’s bullets were unreadable — red text on dark violet — with a QR code sitting on top of the text block.
- Slide 18’s title was crushed onto bullet points three and four.
- The structure agents caught two spoken transitions that pointed at slides which were hidden in the deck — the talk track referenced content the audience would never see — plus an ordering incoherence between the two opening slides.
Every one had survived multiple human passes, including mine. None survived fresh eyes with a rendered frame and a checklist.
And the single best catch was the meta one: an annotation my cleanup pass had reported as eradicated was still sitting on slide 16. The visual agent flagged it from the frame. My own tooling had lied to me, which brings us to the pitfall that caused it.
The vertical-tab pitfall
Reviewer feedback on this deck arrived inside the file: unfilled rectangles pinned to slide corners, each carrying a terse instruction. Useful as review, garbage on a projector — so the build strips them. Removal keyed on the annotation text — and logged complete success while one annotation survived.
Root cause: in python-pptx, a soft line break inside a paragraph comes back as \x0b — a vertical tab — in shape.text_frame.text. The surviving annotation was typed on two lines. My matcher searched for the string with a space where the eye sees a line break, found nothing, and reported the annotation already gone. The XML said “no match”; the pixels said otherwise.
The fix is to never match slide text raw. Normalize aggressively, on both sides of the comparison:
import re
def norm(s: str) -> str:
"""Whitespace-normalize pptx text: \x0b soft breaks, curly quotes, case."""
return re.sub(
r"\s+", " ",
s.replace("\x0b", " ").replace("\u2019", "'")
).strip().lower()
def find_shapes_by_text(slide, needle):
n = norm(needle)
for shape in slide.shapes:
if getattr(shape, "has_text_frame", False) and n in norm(shape.text_frame.text):
yield shape
Two adjacent lessons from the same trench. Extract annotations by style signature, not content — a rectangle with no fill, short text, parked in a corner is an annotation whatever it says; my first content-based scan missed one that shared a slide with another. And python-pptx’s paragraph.runs returns fresh proxies on every access, so p.runs[0] is r is always false — keep-the-first-delete-the-rest logic written by identity deletes everything. Operate by index, at the XML element level.
Safe zones: make the geometry a build failure
Half the blockers were the same defect in different clothes: content wandering into the footer band. So the band became a constraint: the build defines a horizontal safe zone at the bottom of the slide — footer, page number, logo territory — and asserts no content shape’s bottom edge enters it. It is plain EMU arithmetic on shape.top + shape.height, runs in milliseconds, and turns a whole defect class into a build failure before rendering.
Same treatment for contrast: a WCAG relative-luminance check over every explicit run color against its background, thresholded at 4.5:1. And — honesty required — the checker logs the runs it cannot judge because their colors are inherited from the layout. Those go to the render pass. A gate that knows where it is blind is worth two that pretend they see everything.
What it caught, what it cost
In real numbers: one idempotent build script per version, rebuilding a 31-slide validated source into a 24-slide talk; one PNG per slide at 150 dpi; four parallel critique agents, seven reports; eight top-severity blockers on a version two humans had already reviewed; and one tooling bug — the \x0b matcher — caught by the fleet reviewing the tooling’s own output. Every fix landed as a transform in the build script, so every fix is regression-proof: the next rebuild reapplies it from clean state, and the next render pass proves it.
The limits, equally real. Colors inherited at layout level are invisible to run-level scripting — two contrast fixes had to be verified at render because the script could not even see the color it was fixing. Master-level ghosts need manual surgery in the master. One text-extraction transform failed on a dense slide, and the numbers moved to the spoken track — the pipeline told me it had failed, which is the property I care about. And frames are static: animations, transitions, and the projector’s color profile stay outside the pipeline’s view. The dry-run on a real screen stays on the checklist.
Honest recommendation
If you touch a deck more than twice under review, stop editing it by hand — but don’t build the whole pipeline on day one. The 20-line idempotent rebuild script is 80 percent of the value: the validated file becomes untouchable, every change becomes replayable code, and iteration stops being scary. Start there.
Add the render-then-inspect step the first time a “fixed” deck comes back with a defect your structural checks swore was gone — it will happen, because the XML genuinely does not contain what the eye sees. LibreOffice headless plus pdftoppm is boring and reliable.
Add the critique fleet when the deck matters enough that you have stopped seeing it. That threshold is about you, not the deck: after ten passes, the footer collision is invisible to you and obvious to anyone fresh, and a vision model reading rendered frames is the cheapest fresh eyes you can rent — cheap enough to run on every version, which no human reviewer will ever agree to.
And keep one human dry-run at the end. The fleet reviews frames; the talk happens live. Trust the agents to catch what you have stopped seeing, not to replace the one rehearsal in front of a real projector.
ABOUT THE AUTHOR
ONE LETTER A MONTH · NO TRACKER · UNSUBSCRIBE ANYTIME
CONTINUE READING
Related dispatches
From RPA Bots to AI Agents — A 5-Criterion Scoring Framework for Enterprise Migration
5 MIN READ
Validate the Issue, Not the Structure: a Self-Correcting Pipeline for LLM-Generated Games
7 MIN READ
How the Orchestrator Works — The AI Agent That Never Works Alone
9 MIN READ
Comments
Sign in to leave a comment
