← Blog

A product is a character too

A single generate_image(prompt) tool works exactly once. The moment images become a repeated job — the same subject, the same product, ten different settings — it stops being a tool problem and starts being a workflow problem.

Kilimani rooftop — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraKarura Forest — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraMaasai Market — Amara walking, carrying the kiondo tote on her shoulder, looking off camera
Same model, same bag, three locations. Only one argument changed between these calls.

A friend of mine runs a service in Nairobi: businesses send him their inventory, and he produces catalog imagery — the product worn or carried by a model, photographed around the city. Rooftops in Kilimani, the red-earth paths of Karura, the stalls at Maasai Market.

It is a good business and a miserable workflow. Twenty items across five locations is a hundred setups, and each one is a fresh chance for the model's face to shift, the bag to change colour, or the light to stop matching the rest of the set. That is the problem glove-image was built for.

Prompts are built, not typed

The prompt that actually works is never the sentence a user says. It is their intent, plus the house style, plus the character's canonical description, plus the scene's palette, plus whatever rewriting the target model responds to. That is a pipeline with stages, and most implementations inline it into a template string and lose every intermediate state — including the reason the final prompt looks the way it does.

So the pipeline is the primitive. An intent runs through ordered inbetweens, each a small named transform, each appending to a trace:

mount.tstypescript
await mountImage(glove, {
  adapter: openrouterImages(),
  assets, library,
  pipeline: [
    expandCharacters(),   // splice each character's canonical wording, verbatim
    expandScenes(),       // splice the setting block
    styleDirective("editorial catalog photography, natural daylight"),
    llmEnhance(),         // one rewrite pass — forbidden from touching identity wording
  ],
  // fitToModel() is always appended last.
});

The last stage is the one that earns its place. fitToModel() reconciles the request against what the adapter actually supports — folding negatives into the prompt when the model has no negative slot, dropping reference roles it does not honour, snapping sizes, clamping candidates. Every one of those adjustments is written into the trace and handed back to the agent:

tool resultjson
{
  "assets": [
    {
      "id": "img_…",
      "width": 1024,
      "height": 1024
    }
  ],
  "degradations": [
    "expand-characters: Expanded 2 character(s).",
    "expand-scenes: Expanded scene \"kilimani-rooftop\".",
    "fit-to-model: No negative-prompt slot — folded into the prompt as an Avoid clause."
  ],
  "usage": {
    "requests": 1,
    "cost_usd": 0.0391692
  }
}

This matters more than it sounds. A model that silently receives fewer reference images than it asked for produces a confusing result and no explanation. Telling it what changed is the difference between a bug and a fact.

Consistency is repetition, not memory

“Draw Mira again, but at the harbour” only works if Mira is a durable thing. Not a phrase the model half-remembers from six turns ago — an actual record, with wording that does not move.

library.tstypescript
glove_image_character_save({
  name: "amara",
  appearance:
    "a Kenyan woman in her late 20s, warm dark brown skin, short natural afro, " +
    "delicate gold hoop earrings, wearing a fitted cream linen jumpsuit and tan sandals",
  notes: "House model, SS26 line.",   // never sent to the image model
  negative: "no sunglasses, no hat",
})

That appearance paragraph is spliced into every prompt that names this character, word for word. The LLM rewrite pass is explicitly instructed not to touch it, because identity consistency dies in paraphrase — “short natural afro” rewritten to “cropped curls” is a different person.

Kilimani rooftop — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraKarura Forest — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraMaasai Market — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraCBD side street — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraNairobi National Park — Amara walking, carrying the kiondo tote on her shoulder, looking off camera
Five locations, five calls. The gold hoops, the cream linen, the tan sandals and the bag's burnt-orange band are not luck and not a seed — they are the same sentences arriving five times.

A product is a character too

Here is the part that took me a while to see. A character in this system is not “a person”. It is a durable visual identity: wording that must stay stable, reference images that anchor it, and negatives that fence off drift.

A handwoven kiondo tote is exactly that. So is a beaded cuff. So is every item in a client's inventory. The packshot the business already sent becomes the product's identity reference, and the item joins the library beside the model:

Kiondo tote — packshot — Product packshot: handwoven kiondo toteBeaded cuff — packshot — Product packshot: Maasai beaded cuff
The client's inventory photos. In a real workflow these are imported, not generated — and pinned to a character as its identity anchor.
library.tstypescript
glove_image_character_save({
  name: "kiondo-tote",
  appearance:
    "a handwoven Kenyan kiondo tote bag in natural cream sisal with a band of " +
    "burnt-orange and black geometric pattern, tan leather handles and trim",
  ref_images: [{ asset: packshotId, label: "packshot" }],
})

// Then both, in one frame:
glove_image_generate({
  intent: "Amara walking, carrying the tote on her shoulder",
  characters: ["amara", "kiondo-tote"],
  scene: "kilimani-rooftop",
})

Once products are characters, the catalog matrix collapses. Adding a second item is not a second project — it is another name in an array:

Beaded cuff · Kilimani rooftop — Amara resting one hand on a railing, the beaded cuff clearly visible on her wristBeaded cuff · Karura Forest — Amara resting one hand on a railing, the beaded cuff clearly visible on her wristBeaded cuff · Maasai Market — Amara resting one hand on a railing, the beaded cuff clearly visible on her wrist
A second inventory item across the same locations. Twenty items × five locations is a loop over two lists, not a hundred prompt-writing sessions.

Revisions should not re-derive the brief

Clients do not ask for new images. They ask for the same image, but — at dusk, in the rain, with the other colourway. If the only record of how a shot was made is the conversation that produced it, then every revision is a reconstruction.

So every generated asset stores its own Recipe: the intent, the final prompt, the characters and scene, the refs, the full trace, and what it cost. “Same but at dusk” is then one call.

revision.tstypescript
glove_image_regenerate({ asset: "img_…", tweak: "at dusk, city lights just coming on" })
at dusk — Amara walking, carrying the kiondo tote on her shoulder, looking off camera. at dusk, city lights just coming onin soft rain — Amara walking, carrying the kiondo tote on her shoulder, looking off camera. in soft rain, wet terrace reflecting the light
Two revisions of the rooftop shot. The recipe is replayed through the current pipeline — so if a character's definition was corrected in the meantime, the replay picks that up.

The same logic covers house style. Identical intent, identical characters, identical scene — one line of the pipeline changes, and the catalog is re-shot for a different channel:

Editorial catalog — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraGrainy 35mm film — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraHigh-key studio — Amara walking, carrying the kiondo tote on her shoulder, looking off cameraHand-painted gouache — Amara walking, carrying the kiondo tote on her shoulder, looking off camera
One styleDirective, four values: editorial, 35mm film, high-key studio, hand-painted gouache. Nothing else about the call changed.

Know what it cost

Generation spends real money, and a service business needs to price a job before quoting it. Every model-touching call is metered — the generation, the LLM rewrite pass, any vision review — and attributed by source.

The images on this page, and every other image in the gallery, came from one scripted run: 19 images for $0.78, about four cents each, with no hand-picking and no retries. The agent can read the same figures mid-conversation with glove_image_usage; a host can stream them into billing through an onUsage callback.

Where this stops working

These are approximations, not photographs. Woven, textile and craft goods hold up well — the images above are honest about what the pipeline produces. A product carrying an exact logo, a precise brand colourway, or fine hardware detail will not reproduce faithfully, even with its own packshot pinned as an identity reference. For those, treat the output as styling reference and shoot the hero frames properly.

I would rather say that here than have someone discover it on a client job. The useful version of this tool is the one whose limits you know before you quote.

Try it

terminalbash
pnpm add glove-image

The guide covers the full design — pipeline internals, writing your own inbetween, reference roles, editing, assembly, the vision review loop, and the storage seams you replace in production. The gallery shows every image with the prompt that produced it and a canvas drawing one image's real provenance. And examples/image-studio in the repo is a runnable art-director agent that does all of this from plain conversation.

The obvious next thing is video. The package is deliberately scoped to stills for now.