Skip to main content
  1. Posts/

Do not restyle a deck to look official. Wipe the template and keep its masters.

··907 words·5 mins·
Nick Liu
Author
Nick Liu
Building infrastructure for Facebook Feed Ranking at Meta. Previously at Walmart, Twitter, AWS, and eBay. MS in Computer Science at Georgia Tech.
Table of Contents
Auditing AI-Solvability - This article is part of a series.
Part 5: This Article
The last deliverable of my summer project was a slide deck in the official university template. I generate slides from Python, and my first instinct was to rebuild the branding by hand: sample the navy, find the fonts, redraw the footer. That path produces decks that look almost right, which is worse than wrong. The trick that works is to load the official .pptx itself, delete every sample slide while keeping the slide masters, and add my own slides on the official layouts. Backgrounds, fonts, and logo come along for free.

The generator is one Python file, roughly 470 lines of python-pptx, and it built the final presentation for the auditing project from part 1. This post is about the two things in it worth stealing and the one bug worth laughing at.

Wiping slides without leaving corpses
#

python-pptx will happily open the official template as a presentation. The sample slides then need to go, and this is where the subtlety lives:

def wipe_slides(prs):
    lst = prs.slides._sldIdLst
    for sldId in list(lst):
        prs.part.drop_rel(sldId.get(qn("r:id")))   # drop rel too, or you get orphan parts
        lst.remove(sldId)

A .pptx is a zip of XML parts wired together by relationships. Removing a slide’s id from the slide list makes it disappear from the deck, but the underlying part is still referenced by a relationship from the presentation part. Skip the drop_rel line and the file still opens, which is the trap: you ship a package carrying orphaned parts, and whether anything downstream chokes on them is up to tooling you do not control. Drop the relationship and the id together and the package stays clean.

What survives the wipe is the point. Slide masters and layouts are separate parts, untouched by this loop. Every slide I add afterwards goes onto an official layout, which means the background art, the footer, the logo placement, and the typography all come from the university’s own files. Nothing to sample, nothing to eyeball, nothing to drift out of date when the brand team ships a new template. Load, wipe, rebuild.

Colors from the theme, not from the eyedropper
#

The template’s theme defines its palette, so the constants in my file are transcriptions with their theme slots noted, not guesses from a color picker:

NAVY  = RGBColor(0x00, 0x30, 0x57)   # Tech Navy   (dk1)
GOLD  = RGBColor(0xB3, 0xA3, 0x69)   # Tech Gold   (accent1)
GREY  = RGBColor(0x54, 0x58, 0x59)   # Grey Matter (dk2)
BODYFONT = "Roboto"
HEADFONT = "Roboto Slab"

The comments carry the important part. When a shape needs explicit coloring, the value traces back to a named slot in the official theme, so a reviewer can check it and a future me can update it.

The eight-inch hexagon gap
#

Now the bug. Content slides lay out a row of hexagons, and the gap between them was computed from the full slide width:

# before (buggy): with n=2 this gives gap = 8.03 inches
gap = (13.33 - 0.9 - n * w) / max(n - 1, 1) if n > 1 else 0

The formula spreads n hexagons across the entire 13.33 inch slide. With five hexagons that looks intentional. With two, the leftover width all becomes gap: 8.03 inches of it, so the two hexagons were flung to opposite edges of the slide with a hole in the middle, like a divorce rendered in vector graphics. The layout code was not wrong for the case I wrote it for. It was wrong for the case I never tested.

A slide titled “Two stats, one row” where two hexagons sit pinned to the far left and far right edges with an 8-inch empty gap between them

That render is real: for this post I re-ran the generator with the buggy formula restored and two placeholder stats, and the template did exactly what it did in June.

The fix abandons full-width justification for a fixed gap with true centering, and draws the hexagons larger when there are only one or two, since a sparse row can afford the space:

# after
w, h, y = (2.9, 2.5, 2.35) if n <= 2 else (2.2, 1.95, 2.55)
gap = 0.7
x0 = (13.33 - (n * w + gap * (n - 1))) / 2
The same slide after the fix: two larger hexagons grouped side by side in the center of the slide

Same slide source, current code. Justified layout distributes leftover space, and leftover space grows as items shrink in number. Any layout formula that divides remaining width by item count has this failure mode waiting at n=2 and n=1. Centered layout with a fixed gap degrades gracefully instead.

Lessons
#

  • Inherit branding, never imitate it. Load the official file, wipe its slides, keep its masters, and build on its layouts.
  • When you remove a part from an OOXML package, remove its relationship in the same breath. A file that still opens is not evidence the package is clean.
  • Hardcoded colors are debt unless each one names the theme slot it came from.
  • Justified layouts fail at low item counts because leftover space has nowhere to go but the gaps. Test layout math at n=1 and n=2, not only at the count in your mockup.

That closes the series. The auditor, the harness rules, the degenerate kappa, the failed sabotage, and the deck it was all presented on. The run records stay stamped with model and date, so when someone reruns these problems against next year’s models, disagreement with my numbers will be a finding and not a contradiction.

References
#

  • python-pptx documentation (presentations, slide masters, and layout objects)
  • Code shown above: gt-slides/build_deck.py in the project repo, excerpted verbatim
Auditing AI-Solvability - This article is part of a series.
Part 5: This Article

Related

My dotfiles had a no-exceptions test gate. It had never run once.

··1331 words·7 mins
My dotfiles repo has a CLAUDE.md, and the CLAUDE.md has a rule in bold: every commit must pass the test suite, no exceptions. Within the first hour of an audit this July, I learned that this rule had been enforced exactly zero times since the day it was written. The hook file existed, its contents were correct, it even had its executable bit. It was just sitting at a path that yadm stopped reading a major version ago. No error message. No warning. To yadm, a hook in the wrong place and no hook at all are the same thing.

My blog publishes one post a day. I haven't touched the deploy in weeks.

··1104 words·6 mins
In July I went three weeks without opening my blog repo. During those weeks it published two posts, on schedule, each one confirmed live by an automated check, and the only reason I know all this is a green history in the Actions tab. The system's single notification channel is a failure email, and it has never fired. This post is the full pipeline, including the parts that only exist because something went wrong on the way here. 🧪 Tested with Hugo 0.164.0 · Cloudflare Pages The one Hugo fact everything hangs on # Hugo skips content dated in the future unless you pass --buildFuture (docs). That single default turns the date field into a release valve. Merge a post dated next Tuesday and production simply does not contain it: not in the sitemap, not in RSS, not at its URL. It sits in main, invisible, until a build happens after its date.

A crash is never a pass. Three rules that kept 1,095 eval runs honest.

··1099 words·6 mins
The scariest failure mode in an eval harness is not a wrong answer. It is a harness problem wearing a model problem's clothes. A test file that does not run looks exactly like a problem no model can solve. A sandbox flake looks exactly like a failed attempt. Before I trusted any number from my CS1 auditing tool, I had to make those confusions impossible, and it came down to three rules. Auditing AI-Solvability · Part 2 of 5 1 2 3 4 5 Part 1 of this series reported scores from 1,095 sampled solutions. This post is about why I believe those scores. The harness lives in one Python module, and its docstring is a contract I wrote before the code: