Cast portraits round 4: aqua-r4a with the icon's open smile replaces r3a in the final contact sheet

Two variables against aqua-r3a: mouth described positively (negatives dropped), crop stated like the busts. Mouth holds; the body is larger but still not cropped by the bottom edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 06:30:55 +02:00
commit a2a769a51e
7 changed files with 251 additions and 0 deletions

View file

@ -232,3 +232,27 @@ eureka-r2a and void-r3a. No halo shows on any of the round-3 cutouts on a dark o
Still open: Aqua's mouth. A negative ("no beak") does not hold. A positive description of the shape may do
better, for example the icon's wide open smile, or a mouth drawn as a small round "o" with no point.
# Round 4
This round is one render, Aqua only (aqua-r4a), with batch A's shared block byte for byte. It changes two
variables against aqua-r3a, because r3a missed on both:
1. **Mouth**: described positively, as the icon and the sheet draw it: "a wide, soft, open happy smile on a
rounded puffy snout, full rounded lips with rounded corners, showing a dark mouth and a pink tongue". All the
negatives ("no beak, no bill, no legs and no feet") are dropped.
2. **Frame**: a crop stated the way the busts are cropped: his body fills the frame down to the bottom edge,
with the lower belly cut off as the busts are cut at mid-chest.
The scales sentence is the same as in r3a. Files are in `r4/aqua/`, and the 2k original is in
`F:\Projects\cast-art-originals\r4\`. `r4/render_r4.py` and `r4/matte_r4.py` hold the prompt and the matte,
which is the same as in round 2.
| Render | Observation | Agent verdict | Operator verdict |
|---|---|---|---|
| aqua-r4a | The mouth is the icon's: a wide open smile with rounded corners and a pink tongue on a rounded snout, with no point and no beak. The scales, crest fin, cyan-ringed headphones and forked tail are kept. He is larger in frame than r3a, but the whole round body is still shown: it stops about 12% above the bottom edge instead of running off it like the busts. No halo on either checker. | Mouth holds. Frame is partial: closer to the busts' scale, but not cropped. | |
aqua-r4a holds canon better than aqua-r3a, so it replaces r3a in `r3/contact-final.png`. The set is
libby-r2a, heimdall-r2a, kiko-r2a, aqua-r4a, weksa-r2a, nibu-r3a, eureka-r2a and void-r3a. FLUX still prefers
to show a fish whole. Matching the busts' crop exactly would need a stronger framing lever, for example a
tighter shot described as a close-up of his face and upper body.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

View file

@ -0,0 +1,25 @@
# Aqua r4a
## Hypothesis
aqua-r3a missed two things, so two variables change at once. Describing the mouth positively, as the icon and sheet draw it (a wide soft open smile on a rounded snout, showing a tongue), avoids the pointed beak that the 'no beak' negative kept producing. A crop stated like the busts' (body fills the frame to the bottom edge) puts Aqua at the same scale as the rest of the set. Scales line kept from r3a; batch A's shared block unchanged.
## Variable changed
Against aqua-r3a, character block only: (1) mouth: 'a small round fish mouth with soft lips, no beak, no bill, no legs and no feet' became 'a wide, soft, open happy smile on a rounded puffy snout, full rounded lips with rounded corners, showing a dark mouth and a pink tongue' (all negatives dropped); (2) frame: the frame-filling sentence became a crop that fills the frame down to the bottom edge, the lower belly cut off like the busts' mid-chest crop. Scales sentence identical to r3a.
## Render record
- Model: FLUX 3 Image, `POST https://api.bfl.ai/v1/flux-3-image`
- Parameters: aspect_ratio 4:5, resolution 2k, safety_tolerance default, grounding default; no seed field; rendered from scratch with no reference image or edit
- Output: 1824x2288, source format PNG; 2k original kept out of git at F:/Projects/cast-art-originals/r4/aqua-r4a.png
- Committed: aqua-r4a.webp (lossless, 1024 px long side) and aqua-r4a-cutout.png (transparent, 1024 px long side)
- Request id: c05304de-da43-4cd3-8955-f581c031d0fc
- Date: 2026-10-10
- Composition: shared style block (batch A's block, byte-identical to r2a), blank line, character block
## Prompt
Character portrait for a game cast lineup: a bust, head and shoulders cropped at mid-chest by the bottom edge of the frame, the character in three-quarter view turned toward the right side of the frame, the head centred horizontally with its top a small margin below the top edge, the same crop and scale for every character. Super-deformed chibi proportions: the character is two heads tall in total, so the head is very large compared with the small shoulders and narrow torso. Clean 2D anime illustration drawn to be traced into vector art: one bold dark outline of even weight around the whole silhouette, thinner inner lines, flat cel shading with exactly one base tone and one shadow tone per material, light from the upper left. No gradients, no texture, no painterly blending, no glow halos, no pixel art, no 3D rendering. A limited palette of five or six flat colours taken from the character's key colours. Large expressive anime eyes with two white highlights. The background is one perfectly flat, uniform, solid bright chroma-key green from edge to edge: no gradient, no vignette, no floor, no cast shadow, no particles, no props or scenery behind the figure.
Aqua, a cartoon goldfish DJ. He is a fish, so his crop works like the busts: his round body fills the frame down to the bottom edge, the lower belly cut off by the bottom edge the way the busts are cut at mid-chest, the top of his crest fin a small margin below the top edge, his head and body spanning most of the frame width, the tail fin allowed to run off the side edge: a round chubby goldfish with a golden-yellow face and an orange body covered all over in clearly outlined overlapping fish scales, drawn as rows of small curved scale shapes on the body; big round blue eyes with white highlights; a wide, soft, open happy smile on a rounded puffy snout, full rounded lips with rounded corners, showing a dark mouth and a pink tongue; a little orange crest fin on top of the head; large black over-ear studio headphones with cyan rings on the ear cups. Bright orange pectoral fins, one raised, and a flowing forked orange tail fin. Key colours: orange and cyan.

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

View file

@ -0,0 +1,31 @@
"""Round 4: same matte as matte_r2.py on aqua-r4a; contact-final rebuilt with a chosen Aqua.
Usage: matte_r4.py -> cutout and checks for aqua-r4a
matte_r4.py r3a|r4a -> rebuild r3/contact-final.png with that Aqua."""
import pathlib, sys
from PIL import Image, ImageDraw
import matte_r2 as m, matte_r3 as m3
m3.ORIG = pathlib.Path(r"F:/Projects/cast-art-originals/r4")
R4 = pathlib.Path(r"F:/Projects/gamecult-site-wt-portraits/cast-portraits/r4")
def final(aqua):
root = {"r3a": m3.R3, "r4a": R4}[aqua]
picks = [("libby", m3.R2, "r2a"), ("heimdall", m3.R2, "r2a"), ("kiko", m3.R2, "r2a"), ("aqua", root, aqua),
("weksa", m3.R2, "r2a"), ("nibu", m3.R3, "r3a"), ("eureka", m3.R2, "r2a"), ("void", m3.R3, "r3a")]
cw, ch, pad, lab = 400, 500, 16, 44
W = pad + len(picks) * (cw + pad); H = lab + ch + pad
sheet = Image.new("RGB", (W, H), (128, 128, 128)); d = ImageDraw.Draw(sheet); f = m.font(26)
for i, (n, r, suf) in enumerate(picks):
x = pad + i * (cw + pad)
d.text((x, 10), f"{n} {suf}", fill=(20, 20, 20), font=f)
c = Image.open(r / n / f"{n}-{suf}-cutout.png").resize((cw, ch), Image.LANCZOS)
sheet.paste(c, (x, lab), c)
p = m3.R3 / "contact-final.png"; sheet.save(p, optimize=True)
print(f"{p.name}: {sheet.size}, {p.stat().st_size / 1e6:.2f} MB")
if __name__ == "__main__":
if len(sys.argv) > 1:
final(sys.argv[1])
else:
m3.R3 = R4; m3.JOBS = [("aqua", "r4a")]
m3.cutouts()

View file

@ -0,0 +1,171 @@
"""Round 2 cast portraits. Key read from F:/Projects/bfl-api.txt inside this process only; never printed or written."""
import json, time, urllib.request, urllib.error, io, sys, datetime, pathlib, concurrent.futures as cf
from PIL import Image
REC = pathlib.Path(r"F:/Projects/gamecult-site-wt-portraits/cast-portraits/r4")
ORIG = pathlib.Path(r"F:/Projects/cast-art-originals/r4")
ENDPOINT = "https://api.bfl.ai/v1/flux-3-image"
STYLE_T = (
"Character portrait for a game cast lineup: a bust, head and shoulders cropped at mid-chest by the bottom edge of the frame, "
"the character in three-quarter view turned toward the right side of the frame, the head centred horizontally with its top a small margin "
"below the top edge, the same crop and scale for every character. "
"Super-deformed chibi proportions: the character is {N} heads tall in total, so the head is very large compared with the small shoulders and narrow torso. "
"Clean 2D anime illustration drawn to be traced into vector art: one bold dark outline of even weight around the whole silhouette, "
"thinner inner lines, flat cel shading with exactly one base tone and one shadow tone per material, light from the upper left. "
"No gradients, no texture, no painterly blending, no glow halos, no pixel art, no 3D rendering. "
"A limited palette of five or six flat colours taken from the character's key colours. Large expressive anime eyes with two white highlights. "
"The background is one perfectly flat, uniform, solid bright chroma-key green from edge to edge: no gradient, no vignette, no floor, "
"no cast shadow, no particles, no props or scenery behind the figure."
)
RATIO = {"r2a": "two", "r2b": "three"}
C = {}
C["libby"] = ("Libby, a cheerful young librarian-witch, warm and bookish: thick voluminous wavy hair in a deep dusky violet-lavender with a big swept side fringe, "
"gathered into one loose, fat wavy braid that falls forward over her right shoulder; a white crossed-bone hair clip on the left side of her head; "
"a dark purple ribbon bow with a mauve diamond pattern tied at the top of the braid. Round thin black-framed glasses, large grey-violet eyes, "
"small gold drop earrings, pale skin with rosy cheeks, a big open-mouthed happy smile. She wears a high-collared black scholar's robe with wide sleeves, "
"embroidered in gold and dusky mauve diamond and rune patterns, with a violet gem brooch in a gold setting at the collar. "
"She hugs an old brown leather book with gold corners against her chest, a faint violet light along its page edges. Key colours: black and gold with violet.")
C["heimdall"] = ("Heimdall, a stern young gatekeeper: dark brown skin, a mop of messy dark brown curly hair with golden highlights, "
"glowing golden amber eyes, a serious frown, one small gold earring. He wears a high-collared black coat with royal blue lapels and a royal blue cape "
"trimmed in gold filigree, small blue gems on the shoulder armour, and a gold key-shaped clasp at the throat. "
"He holds an ornate golden key raised beside his shoulder. Key colours: gold and cobalt blue.")
C["kiko"] = ("Kiko, an excitable fox-girl streamer: messy white shoulder-length hair, tall white fox ears with pink insides, "
"a white kitsune fox mask with red and magenta markings worn pushed up on top of her head, black hair ornaments with gold bells and magenta tassels, "
"bright cyan eyes, a wide open-mouthed grin with one small fang, one hand raised in a wave. "
"She wears a black kimono-style jacket with magenta and teal trim and a gold bell at the collar. "
"A fluffy white fox tail with pink tips curls up behind one shoulder. Key colours: magenta and cyan.")
C["aqua"] = ("Aqua, a cartoon goldfish DJ. He is a fish, so his whole body is in the frame instead of a bust: a round chubby goldfish with a golden-yellow face "
"and an orange body covered in clearly drawn overlapping fish scales, big round blue eyes with white highlights, a small round fish mouth, "
"no beak, no bill, no legs and no feet; a little orange crest fin on top of the head; large black over-ear studio headphones with cyan rings on the ear cups. "
"Bright orange pectoral fins, one raised, and a flowing forked orange tail fin. Key colours: orange and cyan.")
C["weksa"] = ("Weksa, a tomboyish young woman, an adventurous field archaeologist in the Indiana Jones mould: warm brown skin, a confident lopsided grin, "
"a smudge of dust on one cheek, messy dark brown shoulder-length hair tied partly up in a topknot with turquoise and amber beads and small feathers, "
"large round gold-rimmed glasses, warm amber-brown eyes, dangling turquoise earrings. She wears a cream linen robe with teal embroidered trim, "
"its sleeves rolled up to the elbows, over a weathered brown leather vest, with beaded necklaces and wooden bead bracelets and a leather strap across her chest; "
"a battered brown leather backpack stuffed with rolled scrolls and a small carved idol shows behind one shoulder. "
"She holds up a small paper card with a hand-drawn glyph between two fingers. No hat. Key colours: teal and amber on earthy browns.")
C["nibu"] = ("Nibu, a quiet, serious worldbuilder girl: very long pale lavender hair with a swept side fringe, "
"a pink hair clip on the left shaped like a small data connector plug, a short rectangular plug body with a round pink bead at each end; "
"a large pink ribbon bow on the right, half-lidded violet eyes, a calm unsmiling expression, arms folded. "
"Wears a sleeveless lilac blouse with a frilled collar, detached dark purple sleeves with circuit-pattern embroidery, "
"a small cyan ring device on the shoulder, and a long dark purple coat. Key colours: violet and pink with a cyan accent.")
C["eureka"] = ("Eureka, a neon-noir bug hunter: a young woman with messy shoulder-length black hair with one bright orange streak in the fringe, "
"sharp focused blue-grey eyes, a slight knowing smile. She wears a dark charcoal trenchcoat with the hood up, framing her face, and a high collar; "
"the hood's edge and the coat's seams are piped with ember-orange lines, over tactical gear: chest straps, buckles and pouches. "
"Fingerless gloves; she holds a slim orange metal probe. Key colour: ember orange.")
C["void"] = ("Void, a small hooded shadow figure: a deep indigo-violet robe with a large pointed hood, the face inside the hood completely black "
"with no features except two tall white glowing oval eyes, a thick bright orange scarf wrapped around the neck with frayed ends trailing to one side, "
"dark gloved hands. Key colours: indigo violet and orange.")
HYP = {
"r2a": ("Consistency: one shared style block, byte-identical across all eight prompts, states the proportions as an explicit head count "
"(two heads tall) instead of the bare word chibi that failed in round 1, and fixes framing, line, shading, palette rule and a flat "
"chroma-key green ground for a deterministic cutout. Per-character text describes only the character."),
"r2b": ("Same as r2a with one variable changed: the figure is three heads tall instead of two, testing whether a milder, more achievable "
"ratio is followed more uniformly across the cast."),
}
VAR = {"r2a": "Batch A baseline: shared style block with ratio two heads tall, then the character block.",
"r2b": "Ratio only: 'two heads tall' became 'three heads tall' in the shared block, against the same character's r2a. Character block identical."}
# Round 3: re-render only the batch-A misses with batch A's shared block byte for byte (two heads tall).
RATIO = {"r3a": "two", "r3b": "two"}
FILL = ("fills the frame as fully as the human busts do: the top of the head a small margin below the top edge, "
"the figure spanning most of the frame width, cut by the bottom edge at mid-chest")
C["aqua"] = ("Aqua, a cartoon goldfish DJ. He is a fish, so instead of a bust his round body " + FILL.replace(", cut by the bottom edge at mid-chest", "") +
", with the tail fin allowed to touch the frame edge: a round chubby goldfish with a golden-yellow face "
"and an orange body covered all over in clearly outlined overlapping fish scales, drawn as rows of small curved scale shapes on the body; "
"big round blue eyes with white highlights, a small round fish mouth with soft lips, no beak, no bill, no legs and no feet; "
"a little orange crest fin on top of the head; large black over-ear studio headphones with cyan rings on the ear cups. "
"Bright orange pectoral fins, one raised, and a flowing forked orange tail fin. Key colours: orange and cyan.")
C["void"] = C["void"].replace("Void, a small hooded shadow figure:", "Void, a hooded shadow figure who " + FILL + ":")
NIBU_CLIP_A = "a pink hair clip on the left shaped like a small data connector plug, a short rectangular plug body with a round pink bead at each end; "
NIBU_CLIP_B = "a pink hair clip on the left shaped exactly like a tiny USB cable plug, a rectangular metal-and-pink connector head with a short cable stub, worn sideways in her hair; "
assert NIBU_CLIP_A in C["nibu"]
C["nibu_b"] = C["nibu"].replace(NIBU_CLIP_A, NIBU_CLIP_B)
HYP = {
("aqua", "r3a"): "Batch A's shared block unchanged; the character block states that Aqua fills the frame as the busts do and draws scales as rows of outlined curved shapes, so he matches the batch in scale and gets his canon scales.",
("void", "r3a"): "Batch A's shared block unchanged; the character block states that Void fills the frame as the busts do, so he matches the batch in scale.",
("nibu", "r3a"): "Batch A's shared block unchanged with the round-2 clip wording (which came right once, in nibu-r2b): a fresh render under A's proportions tests whether that wording carries the connector clip.",
("nibu", "r3b"): "Naming a familiar object (a tiny USB cable plug) instead of describing geometry (a rectangular body with a bead at each end) makes FLUX draw a recognisable connector clip.",
}
VAR = {
("aqua", "r3a"): "Against aqua-r2a: character block only. Added the frame-filling statement; scales described as rows of outlined curved scale shapes; 'soft lips' on the mouth. Shared block identical to batch A.",
("void", "r3a"): "Against void-r2a: character block only. 'a small hooded shadow figure' became 'a hooded shadow figure who fills the frame as fully as the human busts do ...'. Shared block identical to batch A.",
("nibu", "r3a"): "None against nibu-r2a: identical prompt, a fresh render (baseline for nibu-r3b).",
("nibu", "r3b"): "Against nibu-r3a: the clip description only. Geometric description became a named object: 'shaped exactly like a tiny USB cable plug, a rectangular metal-and-pink connector head with a short cable stub, worn sideways in her hair'.",
}
# Round 4: Aqua only, two variables against aqua-r3a (mouth described positively; frame filled to the bottom edge).
RATIO["r4a"] = "two"
AQUA_R3 = C["aqua"]
C["aqua"] = ("Aqua, a cartoon goldfish DJ. He is a fish, so his crop works like the busts: his round body fills the frame down to the bottom edge, "
"the lower belly cut off by the bottom edge the way the busts are cut at mid-chest, the top of his crest fin a small margin below the top edge, "
"his head and body spanning most of the frame width, the tail fin allowed to run off the side edge: a round chubby goldfish with a golden-yellow face "
"and an orange body covered all over in clearly outlined overlapping fish scales, drawn as rows of small curved scale shapes on the body; "
"big round blue eyes with white highlights; a wide, soft, open happy smile on a rounded puffy snout, full rounded lips with rounded corners, "
"showing a dark mouth and a pink tongue; a little orange crest fin on top of the head; large black over-ear studio headphones with cyan rings on the ear cups. "
"Bright orange pectoral fins, one raised, and a flowing forked orange tail fin. Key colours: orange and cyan.")
HYP[("aqua", "r4a")] = ("aqua-r3a missed two things, so two variables change at once. Describing the mouth positively, as the icon and sheet draw it "
"(a wide soft open smile on a rounded snout, showing a tongue), avoids the pointed beak that the 'no beak' negative kept producing. "
"A crop stated like the busts' (body fills the frame to the bottom edge) puts Aqua at the same scale as the rest of the set. "
"Scales line kept from r3a; batch A's shared block unchanged.")
VAR[("aqua", "r4a")] = ("Against aqua-r3a, character block only: (1) mouth: 'a small round fish mouth with soft lips, no beak, no bill, no legs and no feet' "
"became 'a wide, soft, open happy smile on a rounded puffy snout, full rounded lips with rounded corners, showing a dark mouth and a pink tongue' "
"(all negatives dropped); (2) frame: the frame-filling sentence became a crop that fills the frame down to the bottom edge, the lower belly cut off "
"like the busts' mid-chest crop. Scales sentence identical to r3a.")
NAMES = ["aqua"]
JOBS = [("aqua", "r4a")]
def prompt_for(n, suf):
return STYLE_T.format(N=RATIO[suf]) + "\n\n" + C["nibu_b" if (n, suf) == ("nibu", "r3b") else n]
def req(url, key, data=None):
r = urllib.request.Request(url, data=json.dumps(data).encode() if data else None,
headers={"x-key": key, "Content-Type": "application/json", "accept": "application/json"})
return json.load(urllib.request.urlopen(r, timeout=60))
def run(job, key):
n, suf = job
d = REC / n; d.mkdir(parents=True, exist_ok=True)
png = ORIG / f"{n}-{suf}.png"
if png.exists():
return f"{n}-{suf}: exists, skipped"
prompt = prompt_for(n, suf)
try:
j = req(ENDPOINT, key, {"prompt": prompt, "aspect_ratio": "4:5", "resolution": "2k"})
except urllib.error.HTTPError as e:
return f"{n}-{suf}: submit HTTP {e.code} {e.read()[:300]!r}"
rid, poll = j.get("id"), j.get("polling_url")
for _ in range(240):
time.sleep(6)
try: s = req(poll, key)
except Exception: continue
st = s.get("status")
if st == "Ready":
raw = urllib.request.urlopen(s["result"]["sample"], timeout=120).read()
im = Image.open(io.BytesIO(raw)); fmt = im.format; im.save(png, "PNG")
txt = (f"# {n.capitalize()} {suf}\n\n## Hypothesis\n\n{HYP[(n, suf)]}\n\n## Variable changed\n\n{VAR[(n, suf)]}\n\n"
f"## Render record\n\n- Model: FLUX 3 Image, `POST {ENDPOINT}`\n"
f"- Parameters: aspect_ratio 4:5, resolution 2k, safety_tolerance default, grounding default; no seed field; "
f"rendered from scratch with no reference image or edit\n- Output: {im.size[0]}x{im.size[1]}, source format {fmt}; "
f"2k original kept out of git at F:/Projects/cast-art-originals/r4/{n}-{suf}.png\n"
f"- Committed: {n}-{suf}.webp (lossless, 1024 px long side) and {n}-{suf}-cutout.png (transparent, 1024 px long side)\n"
f"- Request id: {rid}\n- Date: {datetime.date.today().isoformat()}\n"
f"- Composition: shared style block (batch A's block, byte-identical to r2a), blank line, character block\n\n"
f"## Prompt\n\n{prompt}\n")
(d / f"{n}-{suf}.txt").write_text(txt, encoding="utf-8")
return f"{n}-{suf}: Ready {im.size}"
if st in ("Error", "Failed", "Content Moderated", "Request Moderated", "Task not found"):
return f"{n}-{suf}: {st} {str(s)[:200]}"
return f"{n}-{suf}: timeout"
if __name__ == "__main__":
key = pathlib.Path(r"F:/Projects/bfl-api.txt").read_text().strip()
only = sys.argv[1:]
jobs = [j for j in JOBS if not only or f"{j[0]}-{j[1]}" in only]
with cf.ThreadPoolExecutor(max_workers=6) as ex:
for r in ex.map(lambda j: run(j, key), jobs):
print(r, flush=True)
print("DONE", flush=True)