← Back to blog

18 April 2026 · 620 words

Teaching Gemini to draw vintage travel stamps

technical
64 new destinations meant 64 new passport-style stamps. Running the batch through gemini-2.5-flash-image turned into a weeks-long conversation about spelling, flags, and what a "cream-colored perforated border" means to a model.

Why raster stamps at all

FOMO Sun has had a procedural SVG stamp on the hero card since the prototype days. It's fine. It renders Austrian red-white-red and Swiss cross and Italian flags at any size without a round-trip. But for premium anchors — Milan, Paris, Munich, Innsbruck — a generic SVG with a flag badge does not feel like a passport stamp. It feels like a placeholder.

The raster series exists to give recognizable destinations a recognizable stamp: the Duomo silhouette on Milan, Eiffel on Paris, Frauenkirche on Munich, Villa Melzi on Bellagio, Aiguille du Midi on Chamonix. Generated once, committed to public/stamps/*-vintage.png, keyed in RASTER_STAMPS inside src/components/DestinationStamp.tsx, served statically.

The pipeline: scripts/generate-stamps-gemini.py pulls per-destination scene descriptions from scripts/stamp_scenes.py, loads 5 reference stamps from scripts/stamp-references/, assembles a prompt, and calls gemini-2.5-flash-image.

The reference-anchor trick

Gemini-2.5-flash-image is a multimodal model — you can pass images into the prompt, not just text. The breakthrough was realizing we didn't have to describe the series aesthetic in words. We could just show it.

scripts/stamp-references/ holds six hand-picked stamps from earlier batches that nail the motif: cream scalloped perforated border, warm sepia palette, hand-stamped ink texture, destination name in ALL CAPS across the bottom banner, region in smaller caps below. Every generation call loads those five references plus a per-destination scene sketch. "Match the visual style of these examples" is a much shorter prompt than trying to describe the style.

The flag that wouldn't stop being a no-entry sign

Austrian destinations kept rendering with a red circle with a white horizontal bar — a no-entry traffic sign. The Austrian tricolor is red-white-red horizontal stripes, and Gemini apparently knows "Austria" more strongly as "small landlocked country, sign-shaped" than as its actual flag.

Fix: a seventh reference image, just a clean isolated Austrian flag badge. Once that was in the prompt alongside the aesthetic references, the no-entry artifact stopped showing up. Same technique would generalize to any country with an easy-to-confuse flag.

Spelling is the hard part

Gemini reliably mis-spells long compound names, especially German ones. MEERSBURG became MEESBURG. CERNOBBIO became CERNOBBO. The fix that helped most: a per-destination spelling check block in the prompt that spells the target word letter by letter with spaces between the letters. Something like P A R T E N K I R C H E N. It turns out that formatting helps the model preserve character counts.

That plus an explicit ALL-CAPS enforcement rule for both the destination name and the region subtitle cleaned up most of the typos in the 44-stamp legacy regen pass. But not all of them. A few held out across four or more attempts — see below.

The Lake Como black-dash problem

The Lombardy batch (Bellagio, Lecco, Desenzano, Menaggio) came back with black dashed perforated borders instead of the series' cream ones. Gemini had interpreted "perforated" too literally — actual perforation on a real stamp leaves little dark holes.

Prompt patched to: cream / beige (#F3EBDE) scalloped perforated border, NOT dark / black dots. Gave the color a hex code. Explicitly negated the failure mode. All four regenerated cleanly, plus Sirmione, Bergamo, Brescia in the same pass for style consistency across the Lombardy cluster.

What we couldn't fix

After four regen rounds each, two stamps still ship with visible typos:

  • garmisch-partenkirchen renders name variants of PARTENKCKEN, PARTENCIRKEN, PARTENCRICKEN. Gemini reliably drops or duplicates letters in the second compound no matter how we format the spelling-check block.
  • st-moritz drops the T in the destination name (ST. MORIZ), though the ENGADIN, GR subtitle now renders correctly.

These are the long-compound-name limit of the current model. A proper fix would be a PIL post-processing pass — take the generated stamp, mask out the bottom text banner, redraw it with correct SVG typography composited back over the texture. Deferred. The stamps are visually recognizable and further AI iteration has diminishing returns.

Cost

Around 50 generations across 1.9.0 → 1.9.2 at roughly $0.039/image — about $2 total. Google AI Studio's free tier is generous enough that the real bill is probably zero. Cheap experiment, better-than-expected result, one honest limit we stopped fighting.


Shipped 2026-04-18 as v1.9.1 and v1.9.2. Full release notes in docs/RELEASES.md.