The character-bible technique that keeps one face, one outfit, and one vibe steady across radically different scenes — demoed end-to-end on Nano-Banana via hiapi's async /v1/tasks endpoint for $0.52.

If you have ever tried to generate a recurring character — a storyboard mascot, a brand spokesperson, an e-commerce fit model — you already know the failure mode: every render is "a person who kind of looks the same," and the differences scream the second you put two frames side by side. The Nano-Banana family on hiapi was tuned for exactly this problem. This post is the workflow I actually use:
Nano-Banana ($0.05 flat), Nano-Banana-2 ($0.085 / $0.076 / $0.114 across 1K / 2K / 4K), and Nano-Banana-Pro ($0.17 at 1K and 2K, $0.30 at 4K). All three are async tasks behind POST /v1/tasks.
Consistency is not a single property — it's a small constellation of properties that have to hold across renders for the result to feel like the same person:
The Nano-Banana models on hiapi handle #1, #2, and #3 well when the prompt is dense enough. #4 is the bit that you, the author, hold by writing a character bible that includes posture and expression cues, not just appearance.
Pulled from the live /api/pricing catalogue at the time of writing:
| Model | Resolution tiers | Price (USD per image) | Why pick it |
|---|---|---|---|
Nano-Banana | aspect ratio only, no resolution param | $0.05 flat | Fast iteration. The model you draft the character on. |
Nano-Banana-2 | 1K / 2K / 4K | $0.085 / $0.076 / $0.114 | 4K output, much stronger small-text rendering. The 2K tier is cheaper than 1K — that's not a typo, it's the published policy. |
Nano-Banana-Pro | 1K / 2K / 4K | $0.17 / $0.17 / $0.30 | Sharper detail, deeper micro-features, the most stubborn identity preservation across radical scene changes. |
All three accept input.prompt plus input.aspect_ratio. The Pro and -2 variants additionally accept input.resolution (one of 1K / 2K / 4K). The flat-priced Nano-Banana will reject the resolution field with a 400 — only set it when you've selected one of the tiered models.
My rule of thumb:
Nano-Banana because it's cheap and fast.Nano-Banana-2 at 2K because the policy makes it the price-quality sweet spot.Nano-Banana-Pro for the hero shot — the one frame that ends up on the homepage, the press kit, the printed lookbook.A character bible is a single dense paragraph that you concatenate as a prefix on every prompt for that character. It needs to be specific enough that the model has nowhere to roam, but not so prescriptive that you have no room for the scene.
Here is the exact bible I wrote for the storyboard character in this post:
MIRA = (
"Mira, a 28-year-old field engineer of mixed Filipina–Polish "
"heritage, warm olive skin, narrow oval face, dark almond eyes "
"with light crow's-feet, thick straight black eyebrows, a small "
"mole on the right cheek, shoulder-length wavy black hair tied "
"loosely behind, rust-orange beanie with a small embroidered "
"antenna patch, dust-faded denim jacket over a graphite grey "
"t-shirt, dark cargo trousers, scuffed brown leather work boots, "
"a battered yellow toolbelt, friendly determined expression. "
"Cinematic photography, 35mm lens look, natural film grain, "
"neutral colour grade."
)
Six things this bible does on purpose:
A per-scene prompt is then just: bible + a separator + a short scene paragraph. I use " | Scene: " as the separator. It reads clearly in logs and the model treats the second half as the new request.
All image models on hiapi run through the same async pattern: POST /v1/tasks to create, GET /v1/tasks/{taskId} to poll, and on success read data.output[0].url for the rendered image. The output URL is short-lived (it carries an expireAt), so download the bytes immediately to your storage.
import os, json, subprocess, time
API = "https://api.hiapi.ai/v1/tasks"
TOKEN = os.environ["HIAPI_TOKEN"]
def submit(model: str, prompt: str, aspect_ratio: str,
resolution: str | None = None) -> str:
payload = {
"model": model,
"input": {"prompt": prompt, "aspect_ratio": aspect_ratio},
}
# Only Nano-Banana-Pro and Nano-Banana-2 accept `resolution`.
# The flat-priced Nano-Banana returns 400 on the extra field.
if resolution and model in ("Nano-Banana-Pro", "Nano-Banana-2"):
payload["input"]["resolution"] = resolution
r = subprocess.run(
["curl", "-s", "-X", "POST", API,
"-H", f"Authorization: Bearer {TOKEN}",
"-H", "Content-Type: application/json",
"-d", json.dumps(payload)],
capture_output=True, text=True, timeout=70,
)
data = json.loads(r.stdout)
return data["data"]["taskId"]
def wait(task_id: str, timeout_s: int = 600) -> str:
deadline = time.time() + timeout_s
while time.time() < deadline:
r = subprocess.run(
["curl", "-s", f"{API}/{task_id}",
"-H", f"Authorization: Bearer {TOKEN}"],
capture_output=True, text=True, timeout=40,
)
task = json.loads(r.stdout)["data"]
if task["status"] == "success":
return task["output"][0]["url"]
if task["status"] == "fail":
raise RuntimeError(task["error"])
time.sleep(6)
raise RuntimeError(f"timeout: {task_id}")
def render(model, scene, ratio, bible, resolution=None):
prompt = f"{bible} | Scene: {scene}"
return wait(submit(model, prompt, ratio, resolution))
A 6-second poll interval is the right default for Nano-Banana — a single image lands in roughly 10–60 seconds on a healthy day. Polling tighter than that just wastes round-trips.
The whole point of a storyboard is that the story is what changes between panels — the protagonist is the constant. I'll generate a model sheet and two scene shots, all with the exact same Mira bible.
Before you commit a bible to your repo, render a character sheet from it. This is the cheapest way to find out whether the bible is anchored enough.
sheet = render(
"Nano-Banana", aspect_ratio="3:2", bible=MIRA, scene=(
"a character model sheet on a clean white studio background, "
"three views of Mira side by side — three-quarter view facing "
"left, full profile facing right, three-quarter back view — "
"same outfit, same hairstyle, same toolbelt, even neutral "
"studio lighting, no shadows, no text labels."
),
)

A notable thing happened here: even though the bible explicitly said "Cinematic photography, 35mm lens look," the model produced a 3D toon sheet. That is the single most common failure mode of asking for a "model sheet" or "character sheet" — those tokens drag the aesthetic toward animation industry references in the training set. Two ways to handle it:
"shot on Kodak Portra 400, full-frame DSLR, no illustration, no toon, no 3D render" — usually enough to drag it back to photoreal.I went with option 1 here, because the next two scene shots will be the production frames.
workshop = render("Nano-Banana", "3:2", MIRA, scene=(
"Mira in a cluttered repair workshop, leaning over a wooden "
"workbench under a single warm hanging bulb, reading a circuit "
"schematic on tablet, soldering iron beside her, walls lined "
"with scavenged parts, late evening, focused expression."
))
rooftop = render("Nano-Banana", "3:2", MIRA, scene=(
"Mira on a city rooftop in cold overcast morning light, kneeling "
"to bolt a small antenna mast to a ledge, wind moving her hair, "
"distant grey buildings, breath barely visible, concentrated "
"working expression."
))


The two scenes have nothing in common environment-wise — warm tungsten light in a cramped indoor space versus cold daylight on an open rooftop. But the rust beanie with the antenna patch is the same, the denim jacket is the same, the toolbelt is the same, and the face reads as the same young woman in both. This is the bible doing its job.
| Property | Workshop | Rooftop |
|---|---|---|
| Light temperature | warm tungsten | cool overcast |
| Pose | seated, leaning | kneeling |
| Background | cluttered interior | open city rooftop |
| Time of day | late evening | early morning |
| Beanie | ✅ same rust-orange + patch | ✅ same |
| Denim jacket | ✅ same wash | ✅ same |
| Toolbelt | ✅ same yellow leather | ✅ same |
| Face geometry | ✅ same | ✅ same |
| Beauty mark | present on right cheek | present (slightly fainter) |
This last row is the one that matters most. Across two completely different lighting situations, the small mole stayed on the right cheek. That is exactly the consistency signal you want.
E-commerce is the harder of the two demos in this post. The model has to look literally identical across multiple angles and outfit changes, because the brand's customer is going to A/B these shots in their head on a product detail page. Even small drift breaks the illusion.
Here is the Aria bible:
ARIA = (
"Aria, a 26-year-old fashion catalogue model of Korean descent, "
"soft warm undertone skin, oval face with slightly defined "
"cheekbones, thin straight black eyebrows, dark brown almond "
"eyes, small natural lips with a faint warm pink, a single "
"beauty mark just below the left eye, centre-parted glossy "
"black hair falling straight to mid-back, ears pierced with a "
"single small gold stud each side, no other jewellery, calm "
"neutral expression, even soft studio lighting from front-left, "
"clean light grey seamless paper backdrop, full-body framing, "
"50mm portrait lens look, neutral colour balance, slight matte "
"film finish."
)
Three things this bible does that the Mira bible does not:
frontal = render("Nano-Banana", "3:4", ARIA, scene=(
"full-body straight-on frontal e-commerce shot, Aria standing "
"relaxed with arms at sides, wearing a structured rust-olive "
"wool blazer (single-breasted, two buttons, notch lapel, no "
"pattern) over a plain ivory crew-neck t-shirt and straight-leg "
"dark indigo denim jeans, simple white leather sneakers, clean "
"look-book composition."
))

A small honesty note on the rendered jacket: the prompt asked for rust-olive, and Nano-Banana resolved that toward a warm tobacco-brown. Specific Pantone-style colour names ("rust-olive," "burnt sienna," "ultramarine") don't reproduce reliably in generation. If you need brand-accurate colour, render a base shot then run an image-to-image pass on a colour-specialised model — or just buy a colour-accurate sample at hero-shot time using Nano-Banana-Pro and accept the price.
side = render("Nano-Banana", "3:4", ARIA, scene=(
"full-body three-quarter angle from her right, Aria standing "
"relaxed, wearing the same structured rust-olive wool blazer "
"(single-breasted, two buttons, notch lapel, no pattern) over "
"a plain ivory crew-neck t-shirt and straight-leg dark indigo "
"denim jeans, simple white leather sneakers, hands by her "
"sides, look-book lighting."
))

This is the bit that always trips up character-consistency demos: the same person, rotated. The face has to stay the same, the haircut has to stay the same, and the garment has to wear the same way on her body. Side-by-side, the two shots read as the same shoot — same model, same call sheet, same makeup chair. That's the result you need.
swap = render("Nano-Banana", "3:4", ARIA, scene=(
"full-body straight-on frontal e-commerce shot, Aria standing "
"relaxed with arms at sides, now wearing an oversized "
"camel-coloured double-breasted wool overcoat (mid-thigh length, "
"wide lapels, large mother-of-pearl buttons) over a black "
"turtleneck and straight-leg charcoal trousers, black ankle "
"boots, look-book lighting."
))

This is the demo that earns Nano-Banana its character-consistency tag. Same face, same beauty mark just below the left eye, same hair length and parting, same gold studs, same posture vocabulary — only the wardrobe and the silhouette change. If you're building a virtual-try-on flow, an automated lookbook generator, or a batch-produced PDP shoot, this is the exact transformation you'll be running a few thousand times.
I rendered one extra shot of Mira on Nano-Banana-Pro at 1K, $0.17, just to anchor the cost-quality tradeoff:

Same Mira bible. Same " | Scene: " separator. Only the model name changed. What you actually get for the extra $0.12 per image (Pro at $0.17 vs flat Nano-Banana at $0.05):
For high-volume use (think hundreds of frames per day for a storyboard or for programmatic catalogue generation), flat Nano-Banana at $0.05 is the right default and Nano-Banana-2 at 2K for $0.076 is the production sweet spot. Reserve Pro for the hero frames — the cover image, the lookbook hero, the marketing one-pager.
| Render | Model | Tier | Price |
|---|---|---|---|
| cover (Mira solar farm) | Nano-Banana | 16:9 flat | $0.05 |
| character sheet | Nano-Banana | 3:2 flat | $0.05 |
| storyboard 1 — workshop | Nano-Banana | 3:2 flat | $0.05 |
| storyboard 2 — rooftop | Nano-Banana | 3:2 flat | $0.05 |
| ecomm — frontal | Nano-Banana | 3:4 flat | $0.05 |
| ecomm — side | Nano-Banana | 3:4 flat | $0.05 |
| ecomm — wardrobe swap | Nano-Banana | 3:4 flat | $0.05 |
| pro comparison shot | Nano-Banana-Pro | 3:2, 1K | $0.17 |
| Total | $0.52 |
Eight images, one Bearer token, one HTTP endpoint, no SDK assembly, no upstream account management — that is what hiapi is for.
Nano-Banana for drafts, Nano-Banana-2 at 2K for production batches ($0.076 — cheaper than its own 1K tier), Nano-Banana-Pro for hero shots. Build a tiny dispatcher in your client that picks the model from a quality arg and forwards the right resolution.Nano-Banana rejects resolution. Gate that field on the model name in your client. Hard-learned 400.That's the workflow. Take a bible, pick three identity markers, build a scene fragment, fire it through POST /v1/tasks, and the same person walks out the other end of every render.