Lab 13 — Generate the week’s creative from the box manifest

Scaffold: 2/5. The manifest, the prompt builders and the storage are written. You write the two calls that turn a data file into artwork.

Cost, up front. The stills path is cheap: Stability AI Stable Image Core bills per image and this generates four of them, so the whole run costs cents. The motion path is not: Luma Ray 2 bills per second of output video, and the default manifest produces four five-second clips. Both are third-party models on Amazon Bedrock, so their rates sit alongside the Amazon ones on the Amazon Bedrock pricing page. Check it before you render anything.

Nothing here leaves a meter running. A render bills for the seconds it produced and stops; there is no Provisioned Throughput to forget about. The only cost that outlives the session is S3 storage for what the jobs wrote, and teardown.sh clears that.

The scenario

Greenbox’s marketing artefact changes every week, because the box does. What goes in it depends on what came out of the ground, so the produce list, the featured farms, the suggested recipes and the one vegetable subscribers will not recognise are all different by Monday. Somebody has been briefing a designer every Friday afternoon, and the brief is the same brief every time with different nouns in it.

That weekly change already exists as data. Operations publishes a box manifest so the packing sheets, the delivery notes and the subscriber emails all agree on what is in the box. If the manifest is the source of truth for what is in the box, it can be the source of truth for the picture of what is in the box too.

This lab wires generation onto the end of that pipeline. Four assets come out of one JSON file, unattended, in a consistent illustrated house style.

The requirement

From manifest.json and nothing else:

  1. The box hero. One 16:9 still of this week’s box, with the produce list interpolated into the prompt by code.
  2. Recipe card art. One still per suggested recipe, in the same style family as the hero.
  3. The website motion piece. A short clip per keyframe: the box hero, then an illustrated scene of each featured farm’s crop, animated gently.
  4. The technique clip. Five seconds keyframed on a still of the tricky vegetable mid-prep, looping, because the support inbox fills up with “what do I do with this?” every time kohlrabi goes in the box.

Change the manifest, run it again, get next week’s assets. That is the test.

What’s provided

The house style is a field, not a habit

house_style in the manifest carries the whole look:

"house_style": {
  "style_phrase": "flat vector illustration poster art",
  "palette": "muted garden greens, warm ochre, chalk white, ...",
  "register": "clean shapes and visible drawn edges, ...",
  "negative_text": "photograph, photorealistic, people, faces, hands, farmers, ...",
  "motion": "the camera holds almost still and the illustration barely moves",
  "seed": 733100
}

style_phrase, palette and register are assembled by style_suffix() into one tail that every still gets, so there is exactly one place the look is decided and eight prompts that inherit it. Stable Image Core has no style-preset field, which means the phrasing is the control: naming the medium first and letting the palette and register follow holds a set together better than scattering adjectives through each prompt and hoping.

negative_text is the Greenbox honesty policy written down as code: the real growers appear in real photographs and real footage, and nothing this pipeline produces pretends to be either. A generated farmer on the website would mislead subscribers exactly the way generated walk-through footage of a house misleads a buyer, so people, faces, hands and farmers are all excluded by name. Everything here is artwork of produce, recipes and technique, drawn in a register nobody would mistake for a photograph. That register is the whole of the defence: neither of these models applies the invisible watermark the Amazon generators do, so there is no provenance signal to fall back on if an asset ever left the site looking like a photograph. Keeping the output obviously drawn is what makes the policy enforceable rather than aspirational.

seed is reproducibility, not consistency. Each still derives a stable seed from that base, so a rerun of the same manifest regenerates the same pictures. When a picture changes, the data changed. The video request has no seed field at all, so that guarantee stops at the keyframe: the still is reproducible and the motion is new every render.

Your task

Fill two gaps in src/handler.py. The module docstring has the exact request and response shapes for each.

  1. generate_image(prompt, seed) — call Stable Image Core. It is synchronous: one invoke_model, a base64 PNG in the response. The reply carries images, seeds and finish_reasons as three lists that line up by position, and a non-null finish reason means the filter withheld that image after generating it. Return what arrived, not what you asked for: nothing is raised, so code that reads images[0] and ignores the reasons will publish something the filter already rejected. There is no batch field here either, so one call is one image.
  2. start_clip(prompt, keyframe_png, output_uri, loop) — call Ray 2. One job renders one clip, the keyframe goes in as keyframes.frame0 inline as base64 rather than as an S3 reference, and loop decides whether the first and last frames meet. start_async_invoke hands back an invocation ARN and nothing else; the MP4 is written to the prefix you named.

Every clip is its own job, which is the reason the website piece is a set of clips rather than one long render: a deflected clip costs one clip. Stitching them into a single film is ffmpeg afterwards, not a model feature.

Run it

# Prerequisite: Model access enabled in us-west-2 for BOTH
# stability.stable-image-core-v1:1 and luma.ray-v2:0. They are separate grants.
./scripts/deploy.sh     # stack, manifest upload, handler. Cents.
./scripts/stills.sh     # assets 1 and 2. Cents.
./scripts/test.sh       # runs stills and checks what landed
./scripts/motion.sh     # assets 3 and 4. Gated. Real money.
./scripts/teardown.sh

Defaults are stack genai-lab-13, region us-west-2, stability.stable-image-core-v1:1 for stills and luma.ray-v2:0 for motion. Override with environment variables (STACK, AWS_REGION, IMAGE_MODEL_ID, VIDEO_MODEL_ID, BUCKET_SUFFIX, MANIFEST_KEY).

Ray 2 is served in us-west-2 only, and Stable Image Core is served there too, which is why that is the default here rather than a preference. Check the model-support table by region before you point this somewhere else.

If you have read about this pipeline being built on Amazon Nova Canvas and Nova Reel: it was, and those models are now marked Legacy, with both reaching end of life on 30 September 2026. An account that has not used them recently cannot call them at all, and the request fails with ResourceNotFoundException saying the model is marked by the provider as Legacy. That is why this lab is on Stability and Luma, and it is a fair preview of the maintenance a generation pipeline actually needs.

motion.sh prints the clip count, the seconds of video that implies, and a link to the pricing page, then refuses to move until you type a confirmation. It starts every job, polls each one, and prints what landed. A clip takes a few minutes to render, so expect it to sit there for a while.

What success looks like

Before you fill the gaps, both calls raise NotImplementedError. After:

Then change the data. Swap the substitution, add a recipe, rewrite house_style.palette, then run ./scripts/deploy.sh and ./scripts/test.sh again. The artwork follows. That loop is the point of the whole lab: next week’s creative is a pull request against a JSON file.

If it fails

Reveal the solution

SRC=solution ./scripts/deploy.sh && ./scripts/test.sh

What you just learned