Consulting and Craft · Hands On

Gig Posters From the Listings Database

August 13, 2026 · 16 min read

The Corner Room books live music five nights a week: a couple of headline acts, a residency, an open-mic night, a DJ on Fridays. Every gig deserves a poster, an image for the listing page, and a short loop for the socials, and for years the venue has managed exactly one of those, sometimes, when the booker’s flatmate had an evening free. The listings database, meanwhile, knows every fact a poster needs: the act, the night, the genre, the door time, the price. The gap between “the data already exists” and “the artwork never does” is the whole brief.

This is a build for that gap, and it is worth saying plainly why a gig poster is the rare use case where generation fits without a squint. Most business imagery works by depicting something real: your product, your premises, your people. Generating those misrepresents the world, which is why so many obvious ideas for image generation die on contact with honesty. A gig poster carries no such burden. A century of screen prints and photocopied A4 has trained everyone who looks at one to read it as art about a night, not evidence of anything. The register is illustration, the expectation is illustration, and generation is just a cheaper illustrator.

The data

One row per gig, straight from the listings database the website already renders:

{
  "gig_id": "2026-08-14-marlin-county",
  "act": "Marlin County",
  "support": "The Half Sisters",
  "night": "Friday 14 August",
  "doors": "8pm",
  "price": "AUD$25",
  "genre": "alt-country",
  "mood": "a dusty highway at dusk, neon on the horizon",
  "headline": true
}

The only field that is not already operational data is mood, one line the booker writes when the gig is entered, and it is the creative brief. Everything else on the poster is typography, and typography comes from the data, not the model.

The art: Stable Image Core draws, code sets the type

The one thing diffusion models are famously bad at is the one thing a poster absolutely must get right: the words. Ask a model to render “MARLIN COUNTY, Friday 14 August, doors 8pm” and you will get confident lettering that says something like “MARLIN COUNTRY, Firday 41 Augest”. So the pipeline splits the job the way a print shop would: the model produces the artwork with no text at all, and a compositing step sets the real strings from the database over the top, in the venue’s typeface, the same way every week.

The image model is Stability’s Stable Image Core, which lives in us-west-2, so that is where the Bedrock client points. One call, one image, straight back in the response.

STYLE = "bold graphic screen-print gig poster illustration, flat inks, high contrast"

resp = bedrock_runtime.invoke_model(
    modelId="stability.stable-image-core-v1:1",
    body=json.dumps({
        "prompt": f"{STYLE}: {gig['mood']}, evoking {gig['genre']}, "
                  f"bold shapes, generous empty space top and bottom for type",
        "negative_prompt": "text, lettering, words, numbers, logos, "
                           "people, faces, musicians, instruments being played",
        "aspect_ratio": "16:9",
        "seed": stable_seed(gig["gig_id"], n),
        "output_format": "png",
    }),
)
out = json.loads(resp["body"].read())
art = out["images"][0] if out["finish_reasons"][0] is None else None

Five working details. The prompt asks for empty space because the type has to land somewhere, and asking the composition to leave room beats hoping. negative_prompt carries both the craft rule (no lettering, because it would be gibberish) and the honesty rule, which gets its own section below. There is no style-preset field here, so the house style is a fixed phrase pinned to the front of every prompt; keeping that STYLE string constant holds the venue’s look together across every gig far more reliably than fresh adjectives each week. The seed derives from the gig id, so re-running the pipeline regenerates the same art unless the data changed, and because the model returns exactly one image per call, three candidates means three calls with n running 0 to 2. And finish_reasons is where the safety filter reports itself: a non-null entry means the image was withheld, and the call still succeeds, so the code checks rather than assumes.

Compositing is deliberately boring: the chosen art plus the act, support, night, doors, and price strings from the database, laid out by a fixed template with the venue’s fonts. Pillow or an SVG template both do it in a dozen lines. The poster’s facts are exactly as reliable as the database, because they are the database.

The motion: Luma Ray 2 animates the headline act’s poster

For the weekly socials post, the headline gig’s art becomes five seconds of slow movement. Video generation is an asynchronous job rather than a call you wait on: hand the video model the poster art as its opening keyframe, describe motion that stays close to it, and collect the file from the bucket when the job completes. Ray 2 is also us-west-2 only, which is one less region to think about, since the art was generated there too.

video = bedrock_runtime.start_async_invoke(
    modelId="luma.ray-v2:0",
    modelInput={
        "prompt": f"{gig['mood']}; the scene barely moves, light shifts "
                  f"slowly, a gentle drift, nothing new enters the frame",
        "aspect_ratio": "16:9",
        "loop": True,
        "duration": "5s",
        "resolution": "720p",
        "keyframes": {
            "frame0": {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": "image/png",
                    "data": art_base64,
                },
            },
        },
    },
    outputDataConfig={"s3OutputDataConfig": {"s3Uri": "s3://corner-room-posters/loops"}},
)

get_async_invoke reports the job’s status, the render takes two to five minutes, and the mp4 lands in the bucket. duration takes 5s or 9s and resolution 540p or 720p, so the choices are few and the five-second 720p clip is the obvious one for a feed. loop: True is the one that matters here here: the model renders the clip so its last frame meets its first, which is exactly what a socials loop needs and what an editor would otherwise fake with a crossfade. frame0 pins the opening frame to the approved art, and asking for 16:9 in both places keeps the art and the clip the same shape, so the type template lands where it always lands. The motion prompt is defensive on purpose: each generated second is invented, and the further the camera roams from the keyframe, the more of the frame is the model’s imagination rather than the approved art. “Barely moves” keeps the loop anchored to the poster the booker actually chose; the type gets composited onto the video afterwards, same template, so the words never pass through the model at all.

The rule that makes it work

The negative_prompt excludes people, faces, and musicians, and that exclusion is the entire ethics of the build. A poster’s art is fair game; a generated depiction of the actual band is not, because Marlin County are real people (in the venue’s world) whose likeness the model would be inventing, and a generated crowd shot from “last Friday” would manufacture evidence of a night that looked some other way. The test that sorts every case: is this image supposed to be artwork, or would a reasonable person read it as a record of something real? Poster art passes. Band photos, venue interiors, and crowd shots fail, and they stay photography.

The same test explains why ideas for this technology feel so scarce. Almost everything a business photographs, it photographs because the depiction being real is the point. Generation only fits where illustration was already the honest register: posters, concept art, storyboards, diagrams, mascots, pattern and texture work. The Corner Room’s build works not because generation got good but because the gig poster was already art, already weekly, and already described by a database.

What it costs and what changed

Per gig: three Stable Image Core calls come to cents; the headline loop is a Ray 2 render billed per second of output, a few tens of cents by current rates (the Bedrock pricing page carries the numbers). Per week, the venue’s entire visual output costs less than one hour of anyone’s time, which is the resource that was actually scarce. The booker’s job changed shape rather than disappearing: they write one mood line per gig, pick one of three candidates, and veto anything the house style got wrong. Art direction, five minutes a night, instead of production, never.

What’s worth remembering

  1. Generation fits where illustration is already the honest register; a gig poster is art about a night, and everyone reads it that way.
  2. Let the model draw and let code set the type: diffusion lettering is gibberish, and the poster’s facts should come from the database that already knows them.
  3. Prompt for empty space; type needs somewhere to land.
  4. A fixed style phrase at the front of every prompt is the house style; a seed derived from the record id makes reruns reproducible, so changed art means changed data.
  5. Generate candidates as separate seeded calls and check the finish reason on each, because the safety filter withholds an image without failing the call.
  6. Keyframe the video on the approved art, prompt the motion to barely move, and take the native loop flag when the destination is a feed; every invented second drifts further from what was signed off.
  7. The image model is a synchronous call with the picture in the response; the video model is an asynchronous job that delivers to your bucket.
  8. The honesty test is one question: would a reasonable person read this image as a record of something real? Art passes; generated band photos, interiors, and crowd shots fail, and stay photography.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.