ProjectBuild an Agentic Content Studio: the explanation · Module 1: The problem with making carousels by hand · part 3 of 22
Part 3 · Module 1: The problem with making carousels by hand

Module 1: The problem with making carousels by hand

20 min read·
About 35 minutes · Needs: nothing; start here · Code: research/reference/, prototype/index.html, BUILD_PLAN.md, frontend/src/render/renderer.ts, backend/app/schemas/deck.py

Where this fits

WHERE THIS FITS Slidesmith Studio this module BROWSER React FRONT DOOR AND API nginx, FastAPI BACKGROUND AGENTS LangGraph MODELS Ollama RENDERING Playwright DATA Postgres 17 OPERATIONS evals, Docker, Cloud Run Studio app React pages /render page same renderer nginx proxy, headers Auth sessions, CSRF Brand kit onboarding, uploads Projects deck spec, edits Job runner progress over SSE Research search, facts Strategist outline Designer slide JSON Check fit, else repair LangGraph pipeline state graph, JSON repair Ideas post ideas from pillars qwen3.8:27b Ollama, local GPU Web search Ollama API Chromium render, measure Exports PDF, PNG, ZIP, HTML PostgreSQL users, decks, jobs Uploads disk or GCS Evaluation suites, gate Insights usage, timings Containers Docker Compose Deploy Cloud Run, CI/CD
The parts this module motivates (blue): the studio a person uses, the designer that writes slides, the browser that measures them and the exports that leave the app.

Before any agent or container, there was a person making LinkedIn carousels by hand, with an AI assistant typing HTML. This module looks at that workflow closely, because every part of Slidesmith Studio exists to remove one of its pains. If you understand the problem, the rest of the design stops looking arbitrary.

What it does

THE IDEA Words from the model, pixels from code By hand, or a model writing HTML Slidesmith Studio a 220-line style guide one prompt + settings HTML typed per slide agents write a deck spec brand pasted into every footer brand applied from data overflow judged by eye every slide measured change = edit markup click text, switch template
The studio splits the job: the model (amber) only writes structured content; code (blue) applies the brand, draws, measures and edits. Red marks the hand-made path this replaces.

A LinkedIn carousel is a short deck of square-ish slides, 540 by 675 pixels each, posted as a PDF that readers swipe through. Making a good one takes three different skills: knowing the topic, writing tight copy, and laying it out so nothing spills off the slide. The reference folder in research/reference/ shows how this was done before the studio: a 220-line instruction file describing a design system, and a 594-line Python script that writes every slide as an HTML string.

The obvious shortcut is to ask a language model for the whole thing in one go: "write me a carousel on Kubernetes costs, as HTML". This module explains why that fails, and why the studio instead lets the model write only structured content while code does everything visual. That split, words from the model and pixels from code, is the main idea of the whole project.

At a glance

PracticeWhat it isWhy you need itHow this project does itWhen you need itWhere it lives
Agentic appA model working in several steps, with code checking between them.One model call cannot research, plan, write and check its own layout.Research, plan and design steps, then a code check that can loop back to repair.When a task has stages and a result that can be checked.backend/app/ai/pipeline.py (Module 8)
Content apart from presentationThe model writes a structured spec; code turns it into HTML.A model that writes markup invents styles, breaks layouts and cannot be edited safely.The deck spec: slides, blocks and text fields with limits, drawn by one renderer.Whenever generated content must look consistent.schemas/deck.py, render/renderer.ts (Module 7)
Brand as dataColours, font, name, photo, logo and call to action stored once as a record.A brand pasted into markup drifts and cannot be changed without code.The onboarding questionnaire saves a brand; the renderer themes every slide from it.Any product that makes output for many brands or people.services/brand.py, render/brand.ts (Module 6, brand kit)
Overflow as a measured defectText that does not fit is found by measuring the laid-out slide in pixels.Overflow is invisible in the HTML and easy to miss by eye.Headless Chromium measures every slide; anything more than 2 px over is repaired.Any fixed-size output: slides, labels, certificates, ads.render/measure.ts, render/chromium.py (Module 11)
Prototype before the buildA single-page mock-up that makes the risky parts real before the backend exists.Look, measuring and export are cheaper to get right in one HTML file.prototype/index.html: scripted agents, real rendering, measuring, editing and export.Before a build that depends on a visual or layout idea.prototype/index.html

Use cases and examples

Agentic app

  • Northwind Labs wants a 6-slide carousel on cutting Kubernetes costs. One step plans six slide titles, another writes each slide, and code checks each one fits before the user sees it.
  • A bank's report assistant drafts a monthly risk summary. A planning step lists the sections, a writing step fills each, and code checks that every figure quoted exists in the source tables.
  • A newsroom tool turns a long article into a social thread. The steps are summarise, split into posts, then a code check that no post exceeds the platform's length.

Content apart from presentation

  • An online shop generates product cards. The model writes the name, three features and a price note as fields; the shop's own template draws them, so a model cannot change the font on one card.
  • An insurer writes policy summaries for customers. Fields go into an approved layout that legal has already checked, instead of free HTML that nobody reviewed.
  • A school produces weekly newsletters. Teachers edit fields in a form; the layout stays the same whoever writes it.

Brand as data

  • Tensor Notes (Arjun Mehta, a staff ML engineer) and Northwind Labs use the same studio. Each sees their own colours, font and footer on every slide, with no code changed.
  • A SaaS company with three product lines keeps one brand record per line. Marketing switches the record, not the templates.
  • A government office must use its official colours and crest. Storing them once means every generated notice is correct by construction.

Overflow as a measured defect

  • A designer slide comes back with a fourth card. On a 540 by 675 slide the last card runs under the footer; the check measures it and sends it back for shortening.
  • A hospital prints wristband labels from patient records. A long surname must be measured, not assumed to fit, or part of the name is cut off.
  • An online shop renders banner ads in six sizes. A headline that fits the large banner overflows the small one, and only measuring each size catches it.

Prototype before the build

  • Before writing any backend, the author built the studio's look as one HTML page with a scripted sample deck. The overflow check found a real layout bug there, at no cost.
  • A clinic wants a new booking screen. A clickable mock-up with real date pickers shows the hard cases before the database work starts.

When not to bother. A single carousel you make once a year can be built by hand in a design tool. Plain text posts need no renderer and no measuring. If only one person with one brand ever uses the tool, a brand record is still tidy, but hard-coding is not a disaster.

How it works

We walk through the hand-made workflow that came before the studio, then the problems a "one prompt, finished HTML" shortcut would add, then what the studio does instead. Counts of lines and bytes come from running wc on the reference files on 24 October 2026; timings come from the facts sheet.

Step 1: a style guide written in prose

STEP 1 The rules lived in a 220-line instruction file CLAUDE.md the style guide an assistant read before each carousel Slide size 540 x 675 px Fonts Outfit, Fira Code, Font Awesome Design tokens 17 CSS variables Slide types 6: cover to cheat sheet Components badge, info block, code box, ... Footer one fixed author and logo
Every rule was prose for someone (or a model) to remember. Nothing checked that a slide followed it, and the author and logo were fixed in the text.

The reference folder starts with CLAUDE.md, an instruction file meant for an AI assistant. It fixes the slide size at 540 by 675 pixels, names the fonts (Outfit, Fira Code, Font Awesome icons), lists 17 CSS variables, describes six slide types from cover to cheat sheet, and gives HTML snippets for each component. It also fixes the footer: one author, one photo, one logo.

Notice what this file cannot do. It can describe a rule, but it cannot enforce it. Nothing stops a slide from using a colour that is not in the list, or a card title three lines long.

Step 2: every slide is a string of HTML

STEP 2 One carousel, three files of hand work LINES IN EACH FILE (COUNTED WITH WC, 24 OCT 2026) instruction file 220 build script (Python) 594, 47 KB generated HTML 470, 72 KB 16 slides, every one written as an HTML string
The reference carousel took a 594-line script of HTML strings to produce one 16-slide file. Bars are to scale.

The carousel itself is produced by build_ai_finops_carousel.py, 594 lines and 47 KB. It holds the stylesheet as one long string, a few helper functions (head, card, bottom), and then sixteen S.append(...) calls, each one a slide written as HTML with numbers, icons and chart SVG typed inline. Running it writes ai_finops_carousel.html: 470 lines, 72 KB, 16 slides.

This works, and the result looks good. But the content and the design are woven together. To change one sentence you edit an HTML string inside Python. To change the template you rewrite the stylesheet and probably every slide.

Step 3: the brand is pasted in

STEP 3 The brand was pasted into every slide Footer: one string with a fixed name, photo path and logo path, repeated on all 16 slides. A second brand means a second script. colours 17 CSS variables copied per carousel
The brand (red footers) was text inside the markup. Changing the person, the logo or a colour meant editing code and rebuilding.

The footer of every slide is one Python string, FOOT, with a fixed author name, a profile photo path and a logo path. The colours are CSS variables copied into this carousel's stylesheet. For one person with one brand, that is fine. For a product with many users, it is impossible: each brand would need its own script.

Brand consistency means every slide shows the same colours, font, name and call to action, and that changing the brand changes all of them at once. Hand-made decks get there by discipline. A product has to get there by design.

Step 4: overflow you cannot see

STEP 4 Overflow is invisible in the markup <div class="card"> <div class="h">Idle dev clusters</div> <div class="d">Clusters run all night and every weekend, and the requests were sized for a launch two years ago, so ...</div></div> Valid HTML. Nothing here says it is too long. runs under the footer Only a browser that lays it out sees the spill.
A model writing HTML sees characters, not pixels. The overflow (red) exists only after layout, so it has to be measured in a real browser.

The hardest defect is overflow. A card's text looks harmless as HTML, but once the browser lays it out at the slide's real width, the last lines may run under the footer or past the slide's edge. The HTML is perfectly valid. Only a browser that actually draws the slide, with the real fonts, knows where every line ends.

By hand, the author opens the file and looks. That catches most problems, misses some, and gets slower with every slide.

Step 5: why "one prompt, finished HTML" fails

STEP 5 Six ways "one prompt, finished HTML" breaks FAILURE WHAT THE USER GETS Invented markup class names and styles the template does not have Brand drift colours, fonts and footer change from slide to slide Silent overflow text under the footer, nobody told Edit = regenerate fixing one word means HTML or another model call Format locked in 16:9 from a carousel means writing it all again Unsafe text a <script> in a title runs in the export
Each row is a reason the studio never lets the model write HTML. Every one is handled in code in a later module.

It is tempting to replace the hand work with one model call that returns the finished HTML. Here is what goes wrong, and each row is something this project had to design around:

  • Invented markup. The model uses class names and inline styles that the template never defined, so slides look slightly different from each other.
  • Brand drift. Over a long answer, colours, fonts and the footer change from slide to slide.
  • Silent overflow. The model writes characters, never sees pixels, and cannot know that slide 4 spills.
  • Edit means regenerate. Fixing one word means opening HTML, or asking the model again and hoping nothing else changes.
  • Format locked in. A 16:9 version of the same deck means generating it again.
  • Unsafe text. Any <script> in the output runs when the file is opened.

A local 27B model adds one more: it is slow. On the author's Mac each designed slide took 26 to 67 seconds, so "try again until it looks right" is not a strategy.

Step 6: an agentic app instead

STEP 6 An agentic app: several steps, code in between research model plan model design model check code repair too long: shorten Between model steps, code saves, validates, measures and decides what runs next. The model never decides alone whether the work is finished.
Model steps (amber) do the writing; code steps (blue) check the result and loop back (red) when a slide does not fit.

The studio is an agentic app: a model works through several steps (research, plan, design), and code sits between them, saving, validating and measuring. The model never decides on its own that the work is done. A code step, the layout check, measures each slide, and when one is too tall the graph loops back to a repair step. Module 2 (the solution) gives the full picture, and Module 8 shows the LangGraph pipeline that runs it.

Step 7: a prototype first

STEP 7 A prototype before the build Slidesmith Studio (prototype) Settings prompt, format, template, slides agents scripted fits overflows fits Log steps, timings overflow found render, measure, edit, export: real
The prototype (prototype/index.html) faked only the agents (amber). The look, the overflow check and the exports were real (green), so the hard parts were tried before any backend existed.

Before the backend existed, the author built prototype/index.html: one 573-line page with the studio's layout, a scripted sample deck instead of agents, and everything else real. It renders slides in the three templates, switches format, lets you click text to edit it, measures every slide for overflow with the same element-rect method the product uses, and exports. The build plan calls it "the approved look" and the renderer was ported from it.

The prototype paid for itself. Its first overflow check missed content that spilled out of the top of a slide, a bug that would have been much harder to find inside the full stack (see What can go wrong).

Step 8: brand and limits as data

PRACTICE One brand record themes every slide BRAND (NORTHWIND LABS) primary #01153a secondary #00b0f0 accent #008a9d font Outfit handle @northwindlabs cta Follow Northwind Labs for weekly ... ledger classic midnight same footer, colours and font on all three
In the studio the brand is a record (Module 6), turned into CSS variables at draw time (Module 7). Every template and every slide shows the same footer (green).

In the studio, the brand is a record saved by the onboarding questionnaire (Module 6, onboarding and brand kit). For Northwind Labs it holds the colours #01153a, #00b0f0 and #008a9d, the font Outfit, the handle @northwindlabs and the call to action "Follow Northwind Labs for weekly playbooks". The renderer reads it every time it draws a slide, so ledger, classic and midnight all carry the same footer and colours.

Text limits are data too: a title has 60 visible characters, a card text 110, a check item 90. The prompts quote these numbers to the model, and the layout check measures what actually happened.

Code tour

CODE TOUR Where the problem is written down research/reference/ the hand-made carousel prototype/index.html the approved look BUILD_PLAN.md the contracts render/renderer.ts footer from the brand schemas/deck.py limits as data research/spike_llm.py Module 3: local model
The reference folder (grey) is the old way; the prototype and the build plan set the target; two source files show the fix; the spike belongs to Module 3 (amber).

Most files in this module are not part of the running app: they are the evidence for the problem. Reference files are shown as plain text. Two short excerpts from source-code/ show how the product answers it.

New concept: an agentic app An agentic app gives a model a goal and lets it work in steps, with tools or code in between. The difference from a chatbot is that the app, not the person, decides what happens next: it checks each result and can loop back. Here the steps are research, plan, design, check and repair, and the checks are ordinary code.

The instruction file sets slide size and fonts in prose:

text
### Slide Dimensions
- **Width**: 540px | **Height**: 675px
...
- Code font: `Fira Code` (Google Fonts)
- Icons: Font Awesome 6.4.0

And the build script writes each slide from small helpers that return HTML strings:

text
def head(no, kicker, title):
    return f'<div class="kick"><span class="no">&sect;{no:02d}</span>{kicker}</div><h2 class="t">{title}</h2><div class="dbl"></div>'


def card(icon, h, d, cls=''):
    return f'<div class="card {cls}"><div class="h"><i class="fas {icon}"></i>{h}</div><div class="d">{d}</div></div>'

These helpers are handy, but notice that title, h and d go straight into the HTML. Whatever text you pass, including markup, becomes part of the page, and nothing knows how long d may be.

New concept: separating content from presentation Separating content from presentation means the words and the choices (which block, which icon) are stored as data, and one piece of code turns that data into a visual. A model can then write the data, while the code guarantees the look. The same data can be drawn as a carousel or as 16:9 slides, in any template.

The prototype holds the layout rule that keeps overflow measurable:

text
.s .blocks>*{margin:auto 0}  /* auto margins share free space evenly, and collapse to 0 when content is too tall, so it overflows downward */

In the product, the brand comes from data. This is the footer of every slide:

typescript
// frontend/src/render/renderer.ts
function footerHTML(brand: Brand, deck: Deck, i: number): string {
  const f = brand.footer;
  const name = brand.display_name || brand.brand_name;
  const who: string[] = [];
  const photo = safeImageSrc(brand.photo_url);
  const logo = safeImageSrc(brand.logo_url);
  if (f.show_name && photo) who.push(`<img class="pic" src="${esc(photo)}" alt="">`);
  if (f.show_name && name)
    who.push(
      `<span class="id"><span class="nm">${esc(name)}</span>${brand.handle ? `<span class="hd">${esc(brand.handle)}</span>` : ""}</span>`,
    );
  if (f.show_cta && brand.cta_text) who.push(`<span class="fw">${esc(brand.cta_text)}</span>`);
  if (f.show_logo && logo) who.push(`<span class="lgw"><img src="${esc(logo)}" alt=""></span>`);
  // ...
  return `<div class="foot"><div class="who">${who.join("")}</div><div class="rp">${right}</div></div>`;
}

Code walkthrough

  • footerHTML receives the brand record, the deck and the slide's index. Nothing in it names a person or a logo.
  • name prefers the display name ("Northwind Labs") and falls back to the brand name.
  • safeImageSrc only accepts image addresses the app trusts, such as the user's own uploads; anything else becomes empty (Module 16 explains the attack it blocks).
  • Each if adds one footer part only when the user switched it on in the brand kit: photo and name, handle, call to action, logo.
  • esc(...) escapes every value before it goes into HTML, so a brand name containing <script> is shown as text, never run.
  • The // ... cut builds right: "Swipe" with an arrow on carousel slides, "Repost" on the last one, a page number on 16:9 slides.
New concept: overflow as a defect Overflow is content that leaves the area it must stay in: under the footer, off the right edge, or above the content box. Treating it as a defect means measuring it as a number of pixels on every slide, after real layout with real fonts, and refusing to call a slide finished until the number is small.

Limits live next to the deck spec, as data shared by the prompts and the checks:

python
# backend/app/schemas/deck.py
# Soft length limits in characters of visible text (tags not counted). Used by prompts and `length_issues`.
LIMITS = {"title": 60, "kicker": 40, "lede": 160, "card_h": 32, "card_d": 110, "check_item": 90, "kpi_v": 10,
          "kpi_x": 60, "flow_b": 18, "flow_s": 28, "takeaway": 110}
# Item counts (min, max). Extra items are cut; too few are kept (the layout still renders).
COUNTS = {"cards": (1, 4), "kpis": (2, 3), "bars": (2, 6), "table_head": (2, 4), "table_rows": (2, 8),
          "check": (2, 5), "flow": (2, 4), "blocks": (1, 3)}
CODE_MAX_LINES = 12
MAX_TEXT = 2000          # hard cap on any single text field (abuse guard, far above any layout limit)
MAX_SLIDES = 30

Code walkthrough

  • LIMITS gives every text field a target length in visible characters. These are soft limits: the prompts ask the model to respect them, but text that is longer is kept and left for the layout check to judge.
  • COUNTS gives each block type a range: 1 to 4 cards, 2 to 3 KPIs, up to 8 table rows, and 1 to 3 blocks per slide. Extra items are cut; too few are kept.
  • CODE_MAX_LINES caps a code block at 12 lines.
  • MAX_TEXT and MAX_SLIDES are hard limits against abuse: no field longer than 2,000 characters, no deck longer than 30 slides. Module 7 explains why soft and hard limits differ.

What happens when a card's text is too long

This trace follows the code and test_put_deck_sanitises_and_switches, which saves a card text of 300 characters.

  1. A user (or the designer) puts 300 characters into a card's text, well over the soft limit of 110.
  2. PUT /api/projects/{id}/deck runs normalise_deck. The text is cleaned of markup but not cut: the test checks that all 300 characters are stored.
  3. The editor redraws the slide with renderDeck from the stored spec and the user's brand.
  4. 350 ms after the change, the editor measures the slide in the browser. The card's last lines sit below the content box, so overflow_px is above the 2 px tolerance and the slide's badge turns red with "overflows N px".
  5. During generation, the same measurement runs in the server's Chromium; a slide over the tolerance goes to repair: up to 2 designer "shorten" calls, then up to 3 deterministic trims (Module 11).
  6. Nothing is hidden. If a slide still does not fit, its badge stays red and the user can edit it or ask for "Shorter text".

Design decisions

DecisionChosenAlternativesWhy
What the model writesa structured deck spec (JSON)finished HTMLone renderer for preview, editor and export; edits without HTML; template and format switch without regenerating
Where the brand livesa record per userstrings in the markupone change themes every slide; many users, one code base
How fit is judgedmeasured in Chromium, 2 px toleranceby eye, or by counting charactersoverflow depends on fonts and width; only layout knows
When a slide does not fitrepair with a cap, then show the badgeretry until it fitsa slide takes 26 to 67 s on the local model; an endless loop is not affordable
How to starta one-page prototype with scripted agentsbuild the backend firstlook, measuring and export were proven before any model call
Which modelan open-weight model on the user's Maca hosted API onlyprivate by default and free per call; Module 3 compares them
DESIGN Who writes what The model writes Code writes titles, ledes, card text which block types to use icon names from a list the outline and its order a shorter version on request HTML and CSS of every slide brand colours, font, footer ids, limits, sanitising the measured fit of each slide PDF, PNG, ZIP and HTML files
The model's output is content (amber). Everything a user would call design or format is code (blue), so it is the same every time.

The split in this figure is the contract the rest of the course relies on. Whenever you wonder why a feature is built a certain way, ask which side of this line it sits on. A title's wording is the model's; its font, colour and position are code.

THE COST The price is minutes, not seconds LIVE RUNS ON THE AUTHOR'S MAC, SECONDS (FACTS SHEET) 7 slides, model loaded 414 s 5 slides, cold model 523 s redo one slide 97 s one slide (designer) 26 to 67 s
A local 27B model is slow (amber), and a cold start is slower still (red). The studio streams each slide as it is ready so the wait is visible (Module 13).

The cost of the agentic approach is time. On the author's Mac a 7-slide deck took 414 s with the model already loaded and 523 s for a 5-slide deck from a cold start. That is why the studio streams progress and shows each slide as soon as it is designed (Module 13, live progress), and why a redo touches one slide, 97 s, instead of the whole deck.

When to choose differently

If ...Switch to ...WhyIn this project
you make one bespoke carousel a yeara design tool by handno pipeline beats ten careful minutes for a one-offNot built
decks need more slides than 12a higher slide capthe strategist plans exactly N slides; each costs another designer callSetting: MAX_SLIDES=12
waiting minutes per deck is not acceptablea hosted copy of the same modelmuch faster, but the text leaves your machineSetting: LLM_PROVIDER=openai
a team must share one brandbrand kits shared by a workspacetoday every account has exactly one brandNot built
output is plain text, such as posts or emailsno renderer and no measuringthere is no fixed-size layout to overflowNot built
you need exact pixel art per slidea designer-made template setthe spec's 8 block types cover common layouts, not every ideaBuilt: frontend/src/render/slides.css

What can go wrong

WHAT CAN GO WRONG When a slide does not fit measure overflow_px per slide, in slide pixels tolerance 2 px; sticking out sideways counts as 3 shorten up to 2 designer calls with the overflow trim up to 3 deterministic cuts, re-measured stop still over: the slide keeps its red badge
Overflow is a number, not an opinion. The repair loop (Module 11) has a cap, and a slide that still does not fit is shown to the user, not hidden.

A slide does not fit. The check measures overflow_px per slide; over 2 px goes to repair, which shortens and trims with a cap and re-measures after each round. test_generate_end_to_end_with_retry_icons_and_repair drives a slide that is 48 px over back to the designer and checks that it is re-measured as fit; test_repair_gives_up_after_the_cap checks that the loop stops.

Fixed: overflow escaped through the top.

FIXED IN THE PROTOTYPE Overflow that escaped upwards Before: space-evenly free space spread around the blocks; too tall: spills up first check only looked below top spill was missed After: margin auto 0 auto margins share free space; too tall: they go to 0, spill down check measures above and below every spill is caught
With space-evenly a too-tall slide spilled out of the top too (red), where the first check did not look. Auto margins push all overflow down, and the check now measures both edges (green).

In the prototype, blocks were spaced with justify-content: space-evenly. When the content was too tall, that pushed it out of both the top and the bottom of the content area, and the first check only looked below. It was found while testing the prototype. The fix has two parts: blocks are spaced with margin: auto 0, which shares free space evenly and collapses to zero when there is none, so overflow always goes downward; and the measurement uses the element-rect method, where any element whose box leaves the content area, above or below, counts. Both moved into frontend/src/render/slides.css and measure.ts. There is no jsdom test, because jsdom has no layout engine; the Playwright render tests in tests/test_ai_render.py measure real slides.

Fixed: an invented quote credited to the user's own brand.

FIXED: LIVE RUN An invented quote credited to the user's brand Before "This cut our bill in half." (credited to the user's own brand) research off, so no sources: the quote was made up After prompt: never invent quotes or credit the user's brand guard in code no sources: quote removed test_unsourced_quotes_and_...
A prompt rule alone is advice; the guard in code (green) removes the quote whatever the model writes. The quote text shown is a stand-in, not the live one.

On a live run with research off, so with no sources at all, the designer wrote a customer quote and attributed it to the user's own brand. A carousel like that is worse than useless: it puts words in the author's mouth. The fix has two layers. Every prompt now carries honesty rules (never invent quotes, testimonials, customers or people; never present a made-up number as data; never attribute statements to the user's brand), and a deterministic guard after the model, enforce_honesty, removes quote blocks when there are no sourced facts and logs the change. test_unsourced_quotes_and_slide_numbers_are_removed proves it (Module 10, honesty rules and prompts).

The model writes broken output. Text that is not valid JSON, or a field of the wrong shape, is repaired in code and sent back to the model once with the error; a second failure gives a plain checklist slide from the outline instead of a failed deck (test_designer_failure_uses_fallback_slide).

Markup in the text. Anything other than <b>, <i> and <br> is removed when a deck is saved, and the renderer escapes on draw (test_sanitize_keeps_only_b_i_br, and the security tests in Module 16).

Known limits. Measuring needs a real browser, so the server carries a headless Chromium; without it, slides are marked "fit unknown" rather than checked. The 8 block types cannot express every layout a human designer would draw. And the local model is slow: minutes per deck, not seconds.

Try it yourself

From the project root, look at the size of the hand-made carousel and open it:

bash
cd research/reference
wc -l CLAUDE.md build_ai_finops_carousel.py ai_finops_carousel.html
open ai_finops_carousel.html

What you should see

220, 594 and 470 lines, and a browser tab with 16 carousel slides in the ledger style. Pick a card and imagine changing its text, its brand colour, or the whole deck to 16:9.

Then open the prototype from the project root:

bash
open prototype/index.html

What you should see

The studio's three-column layout. Click Generate: the scripted agents fill the log and the slides appear. Click Check layout: each slide gets a badge, "fits" or "overflows N px". Click into any title and type: the slide changes in place. Switch the template between ledger, classic and midnight, and the format between carousel and 16:9.

Finally, run the tests that pin two of the fixes in this module:

bash
cd source-code/backend
uv run pytest -q tests/test_deck.py
uv run pytest -q tests/test_ai_pipeline.py -k unsourced

What you should see

8 passed for the deck spec tests (sanitising, icons, coercion, length issues), then 1 passed, 20 deselected for the honesty guard. No model, network or Docker is needed.

Check yourself

  1. Why can a model that writes perfect HTML still produce a broken carousel?

Because overflow only exists after layout. The model sees characters, not pixels, so it cannot know that a card's text runs under the footer at 540 px wide with the real font. Only a browser that draws the slide can measure it.

  1. What changes in the studio when Northwind Labs switches from the ledger template to midnight?

Only the template field of the deck spec. The same text, blocks and brand record are drawn again by the same renderer with different CSS, instantly and without calling the model.

  1. Name two things in the reference build script that would have to change for a second brand.

The FOOT string with the fixed name, photo path and logo path, and the colour variables in the stylesheet string. In the studio both come from the user's brand record instead.

  1. Why was a prototype with scripted agents worth building before the backend?

It made the risky, non-model parts real (the look, editing, measuring, export) at almost no cost. Its overflow check found the top-spill bug, which was fixed with auto margins and two-sided measuring before any backend code existed.

  1. A card text is 300 characters long. Does saving the deck fail?

No. Length limits are soft: normalise_deck keeps the text (after sanitising) and the layout check judges whether it fits. Only the hard caps, 2,000 characters per field and 30 slides per deck, cut anything.

Key terms

TermMeaning
Carousela short deck of 540 by 675 px slides posted as a PDF that readers swipe through
Agentic appan app where a model works in several steps and code checks results and decides what runs next
Deck specthe structured JSON a deck is stored as: slides, blocks, text fields, sources and claims
Separating content from presentationstoring words and choices as data and drawing them with one piece of code
Brand consistencyevery slide showing the same colours, font, name and call to action, changed in one place
Overflowcontent that leaves its area on the slide, measured in pixels after layout
Soft limita target length the prompts ask for and the layout check judges, never a reason to reject text
Hard limita cap enforced in code against abuse, such as 2,000 characters per field
Prototypea one-page mock-up that makes the risky parts real before the full build