diff --git a/avian/scripts/build_masks.py b/avian/scripts/build_masks.py new file mode 100644 index 0000000..e629197 --- /dev/null +++ b/avian/scripts/build_masks.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""AvianVisitors - rebuild the collage silhouette masks from the cutouts. + +Step 3 of the illustration pipeline (after pregen.py and cutout.py). + +The collage packs birds by their actual silhouette, not bounding boxes, +so the frontend ships a tiny 1-bit mask per illustration inlined in +apt.js. This reads every cutout in avian/assets/illustrations/ and +rewrites the DIMS and MASKS tables in avian/frontend/apt.js: + + DIMS[slug] = [w, h] aspect, scaled so the long side is 560 + MASKS[slug] = {w, h, bits} silhouette downscaled to <=93px, 1-bit + packed MSB-first row-major, base64. A bit is 1 where + the cutout is opaque (alpha > 127). This is exactly + what loadMask() in apt.js decodes. + +Run after changing the illustration set, then bump SKETCH_VERSION and +IMG_VERSION in apt.js so browsers drop their cached copies. + +Usage: + python3 build_masks.py # rewrite apt.js in place + python3 build_masks.py --check # report only, don't write +""" +from __future__ import annotations +import argparse +import base64 +import json +import re +import sys +from pathlib import Path + +DIM_MAX = 560 # long side of the stored aspect +MASK_MAX = 93 # long side of the stored silhouette +ALPHA_ON = 127 # opaque above this -> silhouette bit set + + +def build_tables(illus_dir: Path): + """Return (dims, masks) dicts keyed by slug, in sorted order.""" + from PIL import Image + dims, masks = {}, {} + pngs = sorted(p for p in illus_dir.glob("*.png") + if re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", p.stem)) + for p in pngs: + slug = p.stem + im = Image.open(p).convert("RGBA") + w, h = im.size + scale = DIM_MAX / max(w, h) + dims[slug] = [round(w * scale), round(h * scale)] + + ms = MASK_MAX / max(w, h) + mw, mh = max(1, round(w * ms)), max(1, round(h * ms)) + alpha = im.getchannel("A").resize((mw, mh), Image.LANCZOS) + px = alpha.load() + bits = bytearray((mw * mh + 7) // 8) + for y in range(mh): + for x in range(mw): + if px[x, y] > ALPHA_ON: + i = y * mw + x + bits[i >> 3] |= 1 << (7 - (i & 7)) + masks[slug] = {"w": mw, "h": mh, "bits": base64.b64encode(bytes(bits)).decode()} + return dims, masks + + +def replace_decl(src: str, name: str, value: str) -> str: + """Replace `var = {...};` (single line) with the new value.""" + pat = re.compile(r" var " + name + r" = \{.*?\};") + repl = f" var {name} = {value};" + new, n = pat.subn(lambda _m: repl, src, count=1) + if n != 1: + raise SystemExit(f"error: could not find `var {name} = {{...}};` in apt.js") + return new + + +def main() -> int: + here = Path(__file__).resolve().parents[1] + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--illustrations", type=Path, default=here / "assets" / "illustrations", + help="Cutout directory (default: avian/assets/illustrations/)") + ap.add_argument("--apt", type=Path, default=here / "frontend" / "apt.js", + help="Frontend file to patch (default: avian/frontend/apt.js)") + ap.add_argument("--check", action="store_true", + help="Report counts and don't write apt.js") + args = ap.parse_args() + + dims, masks = build_tables(args.illustrations) + perched = sum(1 for k in dims if not k.endswith("-2")) + flight = sum(1 for k in dims if k.endswith("-2")) + print(f"built {len(dims)} masks ({perched} perched + {flight} flight) " + f"from {args.illustrations}") + if not dims: + print("error: no cutouts found", file=sys.stderr) + return 1 + + dims_json = json.dumps(dims, separators=(",", ":")) + masks_json = json.dumps(masks, separators=(",", ":")) + + if args.check: + src = args.apt.read_text() + cur = json.loads(re.search(r"var DIMS = (\{.*?\});", src).group(1)) + added = sorted(set(dims) - set(cur)) + removed = sorted(set(cur) - set(dims)) + print(f"apt.js currently has {len(cur)} entries; " + f"+{len(added)} new, -{len(removed)} removed") + if added: + print(" new:", ", ".join(added[:8]) + (" ..." if len(added) > 8 else "")) + if removed: + print(" gone:", ", ".join(removed[:8]) + (" ..." if len(removed) > 8 else "")) + return 0 + + src = args.apt.read_text() + src = replace_decl(src, "DIMS", dims_json) + src = replace_decl(src, "MASKS", masks_json) + args.apt.write_text(src) + print(f"patched {args.apt}\nremember to bump SKETCH_VERSION + IMG_VERSION in apt.js") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/avian/scripts/cutout.py b/avian/scripts/cutout.py new file mode 100644 index 0000000..c531323 --- /dev/null +++ b/avian/scripts/cutout.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""AvianVisitors - cut the cream ground off the generated illustrations. + +Step 2 of the illustration pipeline (after pregen.py, before build_masks.py). + +pregen.py renders each bird on a flat cream ground because the image model +can't cut a clean transparent background on its own (it leaves holes and +fringes). A flat known ground, by contrast, removes cleanly. This runs each +illustration through the BiRefNet matting model (via rembg), then crops to +the bird's bounding box with a small even margin, and saves an RGBA cutout +back in place. + +Idempotent: an illustration that already has a transparent background is +skipped unless you pass --force. + +Requires rembg + onnxruntime (see requirements.txt). The first run downloads +the BiRefNet model (~1 GB) to ~/.u2net/. + +Usage: + python3 cutout.py # process every illustration + python3 cutout.py calypte-anna # one slug (both poses) + python3 cutout.py calypte-anna-2 --force +""" +from __future__ import annotations +import argparse +import sys +from pathlib import Path + + +def main() -> int: + here = Path(__file__).resolve().parents[1] + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("slugs", nargs="*", + help="Slugs to process (e.g. calypte-anna). Default: all.") + ap.add_argument("--dir", type=Path, default=here / "assets" / "illustrations", + help="Illustration directory (default: avian/assets/illustrations/)") + ap.add_argument("--model", default="birefnet-general", + help="rembg model name (default: birefnet-general)") + ap.add_argument("--margin", type=float, default=0.02, + help="Even margin around the bird, as a fraction of its " + "long side (default: 0.02)") + ap.add_argument("--force", action="store_true", + help="Re-cut illustrations that already have transparency") + args = ap.parse_args() + + try: + from PIL import Image + from rembg import new_session, remove + except ImportError: + print("error: needs Pillow + rembg (pip install -r requirements.txt)", + file=sys.stderr) + return 2 + + if args.slugs: + paths = [args.dir / f"{s}.png" for s in args.slugs] + missing = [p for p in paths if not p.exists()] + if missing: + print("error: not found: " + ", ".join(p.name for p in missing), file=sys.stderr) + return 2 + else: + paths = sorted(args.dir.glob("*.png")) + if not paths: + print("error: no illustrations found", file=sys.stderr) + return 1 + + session = new_session(args.model) + done = skipped = 0 + for p in paths: + im = Image.open(p) + if not args.force and im.mode == "RGBA" and im.getchannel("A").getextrema()[0] == 0: + skipped += 1 + continue + cut = remove(im.convert("RGB"), session=session) # RGBA, ground -> transparent + bbox = cut.getchannel("A").getbbox() + if bbox: + pad = round(args.margin * max(bbox[2] - bbox[0], bbox[3] - bbox[1])) + x0, y0 = max(0, bbox[0] - pad), max(0, bbox[1] - pad) + x1, y1 = min(cut.width, bbox[2] + pad), min(cut.height, bbox[3] + pad) + cut = cut.crop((x0, y0, x1, y1)) + cut.save(p) + done += 1 + print(f" [cut] {p.name} -> {cut.width}x{cut.height}") + + print(f"\ncut {done} · skipped {skipped} (already transparent)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/avian/scripts/pregen.py b/avian/scripts/pregen.py index cfbbcb6..0627c01 100755 --- a/avian/scripts/pregen.py +++ b/avian/scripts/pregen.py @@ -1,15 +1,41 @@ #!/usr/bin/env python3 -"""AvianVisitors - pre-generate kachō-e illustrations for a region. +"""AvianVisitors - generate kachō-e bird illustrations for a region. + +Step 1 of the illustration pipeline: + 1. pregen.py render each bird on a uniform cream ground + 2. cutout.py remove the ground (BiRefNet) and crop to the bird + 3. build_masks.py refresh the collage silhouette masks in apt.js Reads a species list (BirdNET-Pi's labels.txt, eBird, or stdin), -generates an illustration for each via the Gemini 2.5 Flash Image API, -and saves PNGs into avian/assets/illustrations/. +fetches a Wikipedia reference photo for each species, and generates an +illustration via the Gemini 2.5 Flash Image API. Saves PNGs into +avian/assets/illustrations/. -Each species gets two poses: .png (perched) and -2.png -(flight). Edit avian/scripts/prompt.template.md to change the visual -style - the prompt body is re-sent verbatim per request with +The prompt renders each bird on a CREAM ground, not a transparent one: +the model can't cut transparency cleanly, but a flat known ground removes +cleanly in step 2. Each species gets two poses: .png (perched) and +-2.png (flight). Edit avian/scripts/prompt.template.md to change the +visual style - the prompt body is re-sent verbatim per request with {sci_name}, {com_name}, and {pose} substituted. +Reference photos: + Cached in avian/assets/references/. The auto-fetch hits the + Wikipedia article's first image. If a reference for the species + doesn't exist locally, pregen.py fetches one and caches it. To use + a hand-picked reference, drop it in references/ named .jpg + or .png BEFORE running and pregen.py will use that instead. + +Contrastive anti-reference: + For genera where Gemini's prior collapses to a more famous + lookalike, the script attaches a photo of that lookalike as a + negative reference and rewrites the prompt body to tell the model + NOT to copy the lookalike's diagnostic features. Currently wired: + Blue Jay for small blue corvids (Cyanocitta, Aphelocoma, etc.) and + Barn Swallow for other swallows (Tachycineta, Progne, etc.). The + anti-reference photos live at avian/assets/references/_anti_*.jpg + and the registry (ANTI_REFS, ANTI_REF_TRIGGERS) is keyed so adding + a new one is one entry per table. + Usage: # Every species BirdNET-Pi knows: python3 pregen.py --labels ~/BirdNET-Pi/model/labels.txt @@ -47,6 +73,143 @@ GEMINI_URL = ( ) POSES = {1: "perched", 2: "in flight with wings spread"} +# Genera where Gemini's prior collapses to Blue Jay markings unless we +# attach a Blue Jay anti-reference. Add to this set if you find another +# blue-songbird genus that needs the contrastive nudge. +JAY_GENERA = { + "Cyanocitta", "Aphelocoma", "Cyanolyca", "Calocitta", "Cyanopica", + "Garrulus", "Cyanocorax", "Gymnorhinus", +} + +# Genera where Gemini's prior collapses to Barn Swallow (rufous throat, +# deeply forked tail) unless we attach a Barn Swallow anti-reference. +# Hirundo rustica is itself the Barn Swallow so it's excluded. +SWALLOW_GENERA = { + "Tachycineta", "Riparia", "Progne", "Petrochelidon", "Stelgidopteryx", +} + +# Genera where Gemini's prior collapses to American Robin (gray back, +# orange breast) for ground-foraging thrushes. Add as needed. +ROBIN_GENERA = set() # placeholder for future use + +# Anti-reference catalogue. Each entry describes one lookalike species +# that Gemini collapses to: the common/scientific names go in IMAGE 2's +# caption and the prompt-body bullet, and `do_not_copy` is the list of +# its diagnostic features the model must avoid. The key matches the +# `_anti_.jpg` filename in the references directory and feeds into +# `ANTI_REF_TRIGGERS` below. +# ---- Style references ---- +# Edo-period kachō-e woodblock prints by Ohara Koson and Hiroshi Yoshida, +# kept in a local directory (default avian/assets/references/styles/). Mapped by +# genus + pose. The bird in each print is irrelevant - only the painting +# technique (flat washes, confident outlines, tonal ground) is borrowed. +STYLE_REFS = { + "small_songbird_perched": "01-sparrows-on-bamboo-Koson.jpg", + "dark_bird_perched": "02-cawing-crow-Koson.jpg", + "vivid_perched": "03-jays-on-berry-tree-Koson.jpg", + "vibrant_perched": "04-kingfisher-Koson.jpg", + "owl": "05-owl-on-ginkgo-Koson.jpg", + "large_flight": "06-goose-flying-in-moonlight-Koson.jpg", + "small_flight": "07-swallows-in-flight-Koson.jpg", + "wader": "08-crane-in-small-water-Koson.jpg", + "pale_perched": "09-cockatoo-Yoshida.jpg", + "waterfowl_perched": "10-mandarin-ducks-Yoshida.jpg", +} + +# Genus → perched style category. The first match wins. Fallback is +# "small_songbird_perched" (Koson sparrows-on-bamboo) for every uncategorized +# genus - covers passer/melospiza/spizella/junco/etc. +GENUS_STYLE_PERCHED = { + # Owls + "Tyto":"owl","Bubo":"owl","Asio":"owl","Megascops":"owl","Athene":"owl", + "Strix":"owl","Glaucidium":"owl","Aegolius":"owl", + # Hummingbirds + jays + colorful crested (vibrant color anchor) + "Calypte":"vibrant_perched","Archilochus":"vibrant_perched", + "Selasphorus":"vibrant_perched","Calothorax":"vibrant_perched", + "Cyanocitta":"vibrant_perched","Aphelocoma":"vibrant_perched", + "Pica":"vibrant_perched","Nucifraga":"vibrant_perched", + "Perisoreus":"vibrant_perched", + # Waxwings + orioles + tanagers (vivid perching with berry-tree composition feel) + "Bombycilla":"vivid_perched","Icterus":"vivid_perched", + "Piranga":"vivid_perched","Pheucticus":"vivid_perched", + "Passerina":"vivid_perched","Cardellina":"vivid_perched", + "Setophaga":"vivid_perched","Icteria":"vivid_perched", + # Corvids + vultures (dark perching) + "Corvus":"dark_bird_perched","Coragyps":"dark_bird_perched", + "Cathartes":"dark_bird_perched","Gymnogyps":"dark_bird_perched", + # Waterfowl perched (mandarin-ducks anchor) + "Anas":"waterfowl_perched","Aix":"waterfowl_perched","Mareca":"waterfowl_perched", + "Spatula":"waterfowl_perched","Branta":"waterfowl_perched","Anser":"waterfowl_perched", + "Cygnus":"waterfowl_perched","Aythya":"waterfowl_perched", + "Bucephala":"waterfowl_perched","Lophodytes":"waterfowl_perched", + "Mergus":"waterfowl_perched","Oxyura":"waterfowl_perched", + "Podiceps":"waterfowl_perched","Podilymbus":"waterfowl_perched", + "Aechmophorus":"waterfowl_perched","Gavia":"waterfowl_perched", + "Pelecanus":"waterfowl_perched","Phalacrocorax":"waterfowl_perched", + "Urile":"waterfowl_perched", + # Waders + herons (crane-in-reeds anchor) + "Ardea":"wader","Egretta":"wader","Bubulcus":"wader","Butorides":"wader", + "Nycticorax":"wader","Plegadis":"wader","Limosa":"wader","Numenius":"wader", + "Himantopus":"wader","Recurvirostra":"wader","Charadrius":"wader", + "Actitis":"wader","Calidris":"wader","Tringa":"wader", + # Pale-bodied (gulls, terns, skimmer - cockatoo anchor) + "Larus":"pale_perched","Leucophaeus":"pale_perched","Sterna":"pale_perched", + "Thalasseus":"pale_perched","Hydroprogne":"pale_perched","Rynchops":"pale_perched", +} + +# Genera that should use large_flight (instead of small_flight) for pose 2. +LARGE_FLIGHT_GENERA = { + "Tyto","Bubo","Asio","Megascops","Athene","Strix","Glaucidium","Aegolius", + "Anas","Aix","Mareca","Spatula","Branta","Anser","Cygnus","Aythya", + "Bucephala","Lophodytes","Mergus","Oxyura","Pelecanus","Phalacrocorax", + "Urile","Ardea","Egretta","Bubulcus","Butorides","Nycticorax","Plegadis", + "Limosa","Numenius","Himantopus","Recurvirostra", + "Buteo","Accipiter","Aquila","Circus","Falco","Cathartes","Coragyps", + "Haliaeetus","Pandion","Elanus","Gymnogyps","Corvus", +} + + +def select_style_ref(sci: str, pose: int) -> str: + """Pick the style reference filename for a (sci, pose) pair.""" + genus = sci.split()[0] + # Custom overrides for hard species + if sci == "Aeronautes saxatalis": + return STYLE_REFS["vibrant_perched"] # Koson kingfisher (vertical aerial-feeder posture, no swallow bias) + if pose == 2: + return STYLE_REFS["large_flight" if genus in LARGE_FLIGHT_GENERA else "small_flight"] + return STYLE_REFS[GENUS_STYLE_PERCHED.get(genus, "small_songbird_perched")] + + +ANTI_REFS = { + "bluejay": { + "common_name": "Blue Jay", + "sci_name": "Cyanocitta cristata", + "do_not_copy": ( + "its facial mask, its white wingbars, its black necklace, " + "its crest pattern, or its white-tipped tail" + ), + }, + "barnswallow": { + "common_name": "Barn Swallow", + "sci_name": "Hirundo rustica", + "do_not_copy": ( + "its deep rufous throat, its long deeply forked outer tail " + "streamers, or its blue-black back" + ), + }, +} + +# Which anti-ref to attach for which genus, and the species to exclude +# (the lookalike itself - we don't want to tell a Barn Swallow generation +# not to look like a Barn Swallow). Order matters only if a genus appears +# in more than one set; first match wins. +ANTI_REF_TRIGGERS = ( + (JAY_GENERA, "bluejay", "Cyanocitta cristata"), + (SWALLOW_GENERA, "barnswallow", "Hirundo rustica"), +) + +USER_AGENT = "AvianVisitors/1.0 (https://github.com/Twarner491/AvianVisitors)" + def slugify(sci: str) -> str: """Match avian/frontend/apt.js slugify() exactly.""" @@ -104,15 +267,211 @@ def ebird_filter(species, region: str, key: str): return [(s, c) for s, c in species if s in allowed] -def gen_one(api_key: str, prompt: str, sci: str, com: str, pose: int) -> bytes: +# ---- Reference photo handling ---- + +# Extensions to scan/write for cached reference photos. .jpg first matches +# the JPEG majority of Wikipedia infoboxes and any legacy hand-placed +# plates; .png covers PNG infoboxes (range maps, some illustrations) and +# hand-placed PNG plates. Each extension here must have a matching MIME +# in _mime_for - keep the two in sync. +REF_EXTS = (".jpg", ".png") + + +def fetch_wikipedia_thumb(sci: str, com: str) -> tuple[bytes, str] | None: + """Fetch the Wikipedia article's lead/infobox image bytes. + + Returns (bytes, ext) where ext is '.jpg' or '.png' sniffed from the + magic bytes - Wikipedia's infobox image can be either, and shipping + PNG bytes labeled as JPEG to Gemini gets the reference silently + rejected. Returns None if no usable image. Pi-friendly: pulls a + 1024-wide thumbnail via the REST summary endpoint (a few KB to MB, + not the original-sized image). + """ + titles = [sci.replace(" ", "_"), com.replace(" ", "_"), com.split()[0]] + for title in titles: + url = ( + "https://en.wikipedia.org/api/rest_v1/page/summary/" + + urllib.parse.quote(title) + ) + try: + req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) + with urllib.request.urlopen(req, timeout=20) as r: + meta = json.loads(r.read()) + except (urllib.error.HTTPError, urllib.error.URLError): + continue + # Prefer originalimage (higher res) over thumbnail. + for k in ("originalimage", "thumbnail"): + src = (meta.get(k) or {}).get("source") + if not src or not src.lower().endswith((".jpg", ".jpeg", ".png")): + continue + try: + req2 = urllib.request.Request(src, headers={"User-Agent": USER_AGENT}) + with urllib.request.urlopen(req2, timeout=30) as r: + data = r.read() + except (urllib.error.HTTPError, urllib.error.URLError): + continue + # Magic-byte sniff - URL extension is a hint, the bytes are + # what Gemini's MIME check sees. Skip unknown formats rather + # than mis-label them. + if data.startswith(b"\x89PNG\r\n\x1a\n"): + return data, ".png" + if data.startswith(b"\xff\xd8\xff"): + return data, ".jpg" + return None + + +def ensure_reference(refs_dir: Path, slug: str, sci: str, com: str) -> Path | None: + """Cache-or-fetch a reference photo. Returns the path if we have one, + None if Wikipedia had no usable image. Pre-existing references (e.g. + hand-picked Audubon plates dropped in by the user as either + .jpg or .png) are respected; the file is saved with the + extension that matches its actual format so _mime_for ships the + right MIME to Gemini.""" + refs_dir.mkdir(parents=True, exist_ok=True) + for ext in REF_EXTS: + cached = refs_dir / f"{slug}{ext}" + if cached.exists() and cached.stat().st_size > 1024: + return cached + fetched = fetch_wikipedia_thumb(sci, com) + if not fetched: + return None + data, ext = fetched + path = refs_dir / f"{slug}{ext}" + path.write_bytes(data) + return path + + +def select_anti_ref_key(sci: str) -> str | None: + """Return the ANTI_REFS key for the lookalike that Gemini drifts + toward for this species, or None if no anti-ref is needed. The key + matches `_anti_.jpg` in the references directory.""" + genus = sci.split()[0] + for genera, key, exclude in ANTI_REF_TRIGGERS: + if genus in genera and sci != exclude: + return key + return None + + +def load_species_notes(notes_path: Path) -> dict[str, str]: + """Load per-species prompt addenda. Keys are scientific names; values + are 1-2 sentence clarifications to inject when generating that + species. Returns {} if the notes file doesn't exist.""" + if not notes_path.exists(): + return {} + raw = json.loads(notes_path.read_text()) + return {k: v for k, v in raw.items() + if not k.startswith("_") and isinstance(v, str)} + + +def load_anti_ref(refs_dir: Path, key: str = "bluejay") -> Path | None: + """Return path to the bundled anti-reference for the given key, + if present. Known keys: bluejay, barnswallow.""" + p = refs_dir / f"_anti_{key}.jpg" + return p if p.exists() else None + + +# ---- Gemini call ---- + +def _anti_ref_line(anti_ref_key: str | None) -> str: + """Render the `{anti_ref_line}` substitution for the prompt body. + Returns the IMAGE 2 bullet describing which species is attached and + which of its features the model must avoid - or an empty string + when no anti-ref is attached for this species.""" + info = ANTI_REFS.get(anti_ref_key or "") + if not info: + return "" + return ( + f"- IMAGE 2 (negative, when attached) is a {info['common_name']} " + f"({info['sci_name']}). It is NOT what you are drawing. Do NOT " + f"copy {info['do_not_copy']}. If your output looks more like " + f"IMAGE 2 than IMAGE 1, the output is wrong." + ) + + +def gen_one( + api_key: str, + prompt: str, + sci: str, + com: str, + pose: int, + positive_ref: Path | None = None, + anti_ref: Path | None = None, + anti_ref_key: str | None = None, + species_note: str | None = None, + style_ref: Path | None = None, +) -> bytes: """Single Gemini call with bounded retry on 429 + transient 5xx. - Returns raw PNG bytes.""" + Returns raw PNG bytes. + + positive_ref: Wikipedia/Audubon photo of the target species. + anti_ref: lookalike photo to attach as IMAGE 2. The companion + anti_ref_key (a key into ANTI_REFS) must match what's in + the file - it drives the IMAGE 2 caption and the + {anti_ref_line} substitution in the prompt body. Pass + both or neither; passing the path without the key would + caption the image as an unnamed "another species". + species_note: optional 1-2 sentence clarifier for difficult species, + appended as the last paragraph before the reference + block. + """ body = (prompt .replace("{sci_name}", sci) .replace("{com_name}", com) - .replace("{pose}", POSES[pose])) + .replace("{pose}", POSES[pose]) + .replace("{anti_ref_line}", _anti_ref_line(anti_ref_key))) + if species_note: + body = body + "\n\nSpecies-specific note: " + species_note + + parts: list[dict] = [{"text": body}] + if positive_ref: + # Downscale the anatomy reference to 384px on the long side + # before encoding. Big Wikipedia photos visually dominate as a + # style signal even though the prompt says they're anatomy-only; + # at 384px the model still reads species/markings/colors but + # has less photographic detail to mimic. + try: + from PIL import Image + from io import BytesIO + img = Image.open(positive_ref).convert("RGB") + w, h = img.size + if max(w, h) > 384: + scale = 384 / max(w, h) + img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS) + buf = BytesIO() + img.save(buf, format="PNG", optimize=True) + ref_bytes = buf.getvalue() + ref_mime = "image/png" + except Exception: + ref_bytes = positive_ref.read_bytes() + ref_mime = _mime_for(positive_ref) + parts.append({"text": "IMAGE 1 (positive, target species):"}) + parts.append({"inline_data": { + "mime_type": ref_mime, + "data": base64.b64encode(ref_bytes).decode(), + }}) + if anti_ref: + anti_name = (ANTI_REFS.get(anti_ref_key or "") or {}).get( + "common_name", "lookalike species" + ) + parts.append({"text": f"IMAGE 2 (negative, {anti_name}, do NOT copy):"}) + parts.append({"inline_data": { + "mime_type": _mime_for(anti_ref), + "data": base64.b64encode(anti_ref.read_bytes()).decode(), + }}) + if style_ref: + parts.append({"text": ( + "IMAGE 3 (positive STYLE reference - Edo-period kachō-e woodblock " + "print). The species in IMAGE 3 is irrelevant; only its painting " + "technique is borrowed (flat washes, confident outlines, tonal " + "mineral-pigment ground). DO NOT copy any branches, leaves, water, " + "moon, or scenery from IMAGE 3.")}) + parts.append({"inline_data": { + "mime_type": _mime_for(style_ref), + "data": base64.b64encode(style_ref.read_bytes()).decode(), + }}) + payload = { - "contents": [{"parts": [{"text": body}]}], + "contents": [{"parts": parts}], # TEXT included so Gemini can surface safety messaging without # rejecting the request shape (image-only sometimes errors). "generationConfig": {"responseModalities": ["TEXT", "IMAGE"]}, @@ -129,7 +488,7 @@ def gen_one(api_key: str, prompt: str, sci: str, com: str, pose: int) -> bytes: backoff = 4.0 for attempt in range(4): try: - with urllib.request.urlopen(req, timeout=120) as r: + with urllib.request.urlopen(req, timeout=180) as r: resp = json.loads(r.read()) break except urllib.error.HTTPError as e: @@ -161,6 +520,17 @@ def gen_one(api_key: str, prompt: str, sci: str, com: str, pose: int) -> bytes: raise RuntimeError(f"no image (finish={finish} block={block})") +def _mime_for(p: Path) -> str: + ext = p.suffix.lower() + if ext in (".jpg", ".jpeg"): + return "image/jpeg" + if ext == ".png": + return "image/png" + if ext == ".webp": + return "image/webp" + return "application/octet-stream" + + def main() -> int: ap = argparse.ArgumentParser( description=__doc__, @@ -177,13 +547,24 @@ def main() -> int: ap.add_argument("--out", type=Path, default=Path(__file__).resolve().parents[1] / "assets" / "illustrations", help="Output directory (default: avian/assets/illustrations/)") + ap.add_argument("--refs", type=Path, + default=Path(__file__).resolve().parents[1] / "assets" / "references", + help="Reference photo cache directory (default: avian/assets/references/)") + ap.add_argument("--styles", type=Path, + default=Path(__file__).resolve().parents[1] / "assets" / "references" / "styles", + help="Style reference directory (default: avian/assets/references/styles/)") ap.add_argument("--prompt", type=Path, default=Path(__file__).resolve().parent / "prompt.template.md", help="Prompt template path") + ap.add_argument("--notes", type=Path, + default=Path(__file__).resolve().parent / "species-notes.json", + help="Per-species prompt addenda for difficult cases (e.g. similar-species drift)") ap.add_argument("--poses", nargs="+", type=int, default=[1, 2], choices=list(POSES.keys()), help="Which poses to render. 1=perched, 2=flight. Default: both.") ap.add_argument("--force", action="store_true", help="Re-render even if file exists") + ap.add_argument("--no-refs", action="store_true", + help="Skip the Wikipedia reference fetch (faster, lower-quality output)") ap.add_argument("--sleep", type=float, default=6.0, help="Seconds between API calls (default 6 = headroom under free-tier RPM cap)") ap.add_argument("--limit", type=int, default=0, help="Cap species count for testing") @@ -220,14 +601,37 @@ def main() -> int: prompt = load_prompt(args.prompt) args.out.mkdir(parents=True, exist_ok=True) + anti_paths: dict[str, Path] = {} + if not args.no_refs: + for key in ANTI_REFS: + p = load_anti_ref(args.refs, key) + if p: + anti_paths[key] = p + notes = load_species_notes(args.notes) + if notes: + print(f"[notes] loaded per-species addenda for {len(notes)} species") total = len(species) * len(args.poses) print(f"generating up to {total} illustrations into {args.out}/") + for key, p in anti_paths.items(): + print(f"[refs] {ANTI_REFS[key]['common_name']} anti-reference: {p.name}") done = skipped_existing = failed = 0 first_fail = None for idx, (sci, com) in enumerate(species): slug = slugify(sci) + pos_ref = None + if not args.no_refs: + pos_ref = ensure_reference(args.refs, slug, sci, com) + if not pos_ref: + print(f" [warn] no Wikipedia photo for {sci} - proceeding without positive ref", file=sys.stderr) + anti_key = select_anti_ref_key(sci) + anti = anti_paths.get(anti_key) if anti_key else None + # Pair the key with the path so gen_one captions IMAGE 2 with the + # right species (the bug this fixes: stale Blue-Jay caption on + # an attached Barn-Swallow anti-ref). + anti_key_for_call = anti_key if anti else None + for pose in args.poses: fname = f"{slug}.png" if pose == 1 else f"{slug}-{pose}.png" path = args.out / fname @@ -235,10 +639,20 @@ def main() -> int: skipped_existing += 1 continue try: - data = gen_one(gemini_key, prompt, sci, com, pose) + style_ref_path = args.styles / select_style_ref(sci, pose) + if not style_ref_path.exists(): + style_ref_path = None + data = gen_one(gemini_key, prompt, sci, com, pose, + positive_ref=pos_ref, anti_ref=anti, + anti_ref_key=anti_key_for_call, + species_note=notes.get(sci), + style_ref=style_ref_path) path.write_bytes(data) done += 1 - print(f" [ok] {fname} ({len(data)//1024} KB)") + refs_tag = "+ref" if pos_ref else "" + anti_tag = "+anti" if anti else "" + note_tag = "+note" if notes.get(sci) else "" + print(f" [ok] {fname} ({len(data)//1024} KB){refs_tag}{anti_tag}{note_tag}") except (urllib.error.HTTPError, urllib.error.URLError, RuntimeError) as e: failed += 1 first_fail = first_fail or fname diff --git a/avian/scripts/prompt.template.md b/avian/scripts/prompt.template.md index 449fcac..ad8dd94 100644 --- a/avian/scripts/prompt.template.md +++ b/avian/scripts/prompt.template.md @@ -1,25 +1,60 @@ # Bird illustration prompt -The prompt sent to Gemini for every illustration. Edit the body to change the style. +The prompt sent to Gemini for every illustration. -Three placeholders get replaced per request: +Three text placeholders get replaced per request: - `{sci_name}` is the binomial Latin name, e.g. `Calypte anna` - `{com_name}` is the English common name, e.g. `Anna's Hummingbird` - `{pose}` is either `perched` (pose 1) or `in flight with wings spread` (pose 2) -The default below is kachō-e (Edo-period Japanese flower-and-bird woodblock prints). Replace it with whatever style fits your apartment. +`pregen.py` also attaches up to three reference images per request: + +- A POSITIVE anatomy reference (Wikipedia photo of the target species). Anchors species identity, markings, and plumage. +- An OPTIONAL anti-reference (a photo of a similar-looking species the model drifts toward). Attached automatically for genera where the prior collapses: a Blue Jay for small blue corvids, a Barn Swallow for other swallows. The `{anti_ref_line}` placeholder is rewritten per-species. +- A POSITIVE style reference (a real Edo-period kachō-e print by Ohara Koson or Hiroshi Yoshida). The species in the print is irrelevant - only its painting style is borrowed. --- ## Prompt -Generate a {pose} {com_name} ({sci_name}) in the style of an Edo-period Japanese kachō-e woodblock print. Confident sumi-e ink linework with soft watercolor washes. Earthy, restrained palette: burnt umber, ochre, indigo, vermillion, muted greens. Plumage details rendered with short directional brush strokes; eye, beak, and feet drawn with crisp ink. The bird is the only subject. NO background, NO branch unless the pose requires it (a single sparse twig is fine for perched), NO border or frame, NO text or signature. +Generate a {pose} {com_name} ({sci_name}) in the style of an Edo-period Japanese kachō-e woodblock print, matching the painting technique of IMAGE 2 closely. Look at IMAGE 2: the bird is rendered with VERY FEW MARKS. The body is essentially 2-4 flat color zones with sharp boundaries. There is almost no internal texture on the body - no feather-by-feather rendering, no pen-line stippling, no gradient shading. The bird in IMAGE 2 looks like it was painted with maybe 30 brush strokes total. YOUR output should look the same: a few flat color zones, a few confident outline strokes, an accent stroke or two for major wing or tail markings, and that's it. -Anatomy must be biologically accurate for the named species: +Confident sumi-e ink linework with soft watercolor washes. Earthy, restrained palette: burnt umber, ochre, indigo, vermillion, muted greens. The body should look like flat painted paper - not a textured surface, not shaded volume. If the species has subtle plumage variation (streaking, mottling, fine barring), ABSTRACT it into 2-3 broad zones rather than rendering it literally. Eye, beak, and feet drawn with crisp ink - these are the only places where confident dark line is appropriate. -- Exactly two wings. Two legs. One head. One beak. One tail. -- Posture, color, markings, and body proportions matching {com_name} field-guide references. -- For perched poses: one wing folded against the body, the other tucked behind. For flight: both wings extended in a natural flapping position. +The bird sits on a CONSISTENT WARM CREAM tonal background - like aged Japanese mulberry paper, a soft warm buff cream color. The cream ground fills the entire frame as the background and is identical across every print for visual consistency. This is the only background element: NO branch, NO twig, NO perch, NO leaves, NO foliage, NO substrate, NO scenery, NO sky, NO moon, NO water - only the bird floating against the cream paper ground. The perch is purely implied by toe posture - it is NEVER rendered. NO border or frame, NO text or signature. + +Composition: the bird occupies one-third to one-half of the frame. Leave generous negative space (just the cream ground) around it. The image should feel sparse and confident, not packed with detail. + +The ENTIRE bird must fit within the image frame: head, both wings (fully extended for flight pose), full tail, both legs, both feet, beak. Do NOT crop the wings, tail, legs, or any body part at the edge of the frame. Leave generous padding on all sides. + +### Reference handling + +- IMAGE 1 (positive, anatomy) IS {com_name}. Match its proportions, head color, throat, wing pattern, back color, tail pattern, leg color. If the reference shows non-breeding or worn plumage, render the brightest BREEDING (adult-summer) plumage instead - render the most diagnostic, recognizable version of the species. +{anti_ref_line} +- IMAGE 3 (positive, style) is a real Edo-period kachō-e woodblock print. The bird in IMAGE 3 is a DIFFERENT species - IGNORE its species, only borrow its painting style. Render the bird in IMAGE 3's painting style. DO NOT copy any compositional elements from IMAGE 3 (branches, leaves, water, moon, scenery). + +Treat IMAGE 1 for anatomy and color information ONLY. Treat IMAGE 3 for style ONLY. The output should look like an Edo-period woodblock print of the species in IMAGE 1, painted by the artist of IMAGE 3. + +### Anatomy + +- EXACTLY TWO wings (no more, no less - count them: one left, one right). EXACTLY TWO legs. EXACTLY ONE head. EXACTLY ONE beak. EXACTLY ONE tail. +- Posture, color, markings, and body proportions match IMAGE 1 / {com_name} field-guide references. +- Pay particular attention to species-specific patterns. Do NOT default to generic markings: if the reference shows a uniformly dark head, do NOT add a white face mask. If the reference shows solid wings, do NOT add white wingbars. If the reference has no crest, do NOT add a crest. +- For close-relative species (multiple goldfinch, multiple jay, multiple sparrow species in the library), render the diagnostic differences clearly so the species are visibly distinguishable from each other. + +### Feet + +- BOTH FEET visible at the bottom of the body. +- Songbird feet are SMALL relative to the body. Tarsi (legs below the feathers) are roughly 10-15% of body height for finches/sparrows/warblers/chickadees, 15-20% for jays/thrushes/mockingbirds. For larger birds (ducks, hawks, owls), match the proportion in the reference photo - typically still under 25%. +- Draw slim tarsi, small delicate toes. Do NOT exaggerate feet or claws; the bird is not a chicken or a crab. +- Match foot proportion to the attached reference photo. + +### Pose + +- PERCHED (pose 1): one wing folded against the body, the other tucked behind. Both feet visible at the bottom, toes curled gently forward as if grasping a thin perch - but the perch itself is NOT drawn. The bird floats in space, posture suggesting it's perched. +- IN FLIGHT (pose 2): both wings fully extended in a natural flapping position. Legs and feet either (a) tucked tight against the belly with toes folded out of sight, or (b) extended straight back along the line of the tail. Do NOT dangle the feet below the body with toes splayed. + +### Output Render at high resolution on a fully transparent background. Cut the bird out cleanly. No shadow, no paper texture, no caption. diff --git a/avian/scripts/requirements.txt b/avian/scripts/requirements.txt new file mode 100644 index 0000000..da2d454 --- /dev/null +++ b/avian/scripts/requirements.txt @@ -0,0 +1,8 @@ +# Image pipeline dependencies for avian/scripts/. +# pregen.py needs only Pillow (for reference downscaling); it degrades +# gracefully without it. cutout.py needs rembg + onnxruntime for the +# BiRefNet matting model. build_masks.py needs Pillow. verify.py is +# standard-library only. +Pillow>=10.0 +rembg>=2.0.76 +onnxruntime>=1.16 diff --git a/avian/scripts/species-notes.json b/avian/scripts/species-notes.json new file mode 100644 index 0000000..8671b0e --- /dev/null +++ b/avian/scripts/species-notes.json @@ -0,0 +1,23 @@ +{ + "_README": "Per-species prompt addenda built during the full restyle review pass. Each entry is appended to the prompt as a one-line clarification when generating that species.", + "_built_during": "2026-06-06 full-restyle review loop", + "Acanthis flammea": "Common Redpoll, adult male (Acanthis flammea). NOT a rosefinch, NOT a Hoary Redpoll, NOT a Lesser Redpoll. Render the SMALLEST possible bill of a finch \u2014 tiny, dainty, fine yellow-horn cone barely longer than its width. NOT a robust seed-eater bill.\n\nCRITICAL FACIAL FEATURES \u2014 read these very carefully (most attempts fail by inventing rosefinch features):\n(1) The RED is a SMALL crimson FORECROWN PATCH only \u2014 a small triangular patch on top of the forehead just above the bill, NOT a full cap covering the whole head. Imagine a beret tipped forward. The red sits on the FOREHEAD/CROWN-FRONT only \u2014 the back-of-head and nape are plain pale brown-buff, NOT red.\n(2) The BLACK is a TINY chin spot UNDER the bill \u2014 small, no larger than the eye itself, sitting at the base of the lower mandible.\n(3) ABSOLUTELY NO white supercilium (eyebrow stripe). NO white line above the eye. The face is plain \u2014 pale buff with maybe a faint paler eye-ring at most.\n(4) ABSOLUTELY NO dark eye-mask, NO dark eye-line through the eye, NO dark lores. The face is plain buff. The eye is small and dark with a thin dark line of lashes only.\n(5) ABSOLUTELY NO gray cheek patch, NO contrasting cheek panel.\n(6) Tiny pale conical YELLOW-HORN bill.\n\nPLUMAGE:\n(7) Pink-flushed BREAST (adult male) \u2014 pink on upper breast only, fading to whitish belly.\n(8) Pale STREAKY brown back, brown wings.\n(9) Two thin pale WHITE WINGBARS as ONE flat brown wing shape with TWO thin white strokes across it.\n(10) Buff-whitish flanks with thin streaks.\n(11) Notched dark brown tail.\n(12) Pinkish-grey thin legs.\n\nSTRICT ANTI-DRIFT: This is NOT a rosefinch (Carpodacus or Uragus), NOT a House Finch. Rosefinches have an extensive red/pink hood, a thicker conical bill, and often a white supercilium \u2014 Redpolls do NOT. NOT a Long-tailed Rosefinch. NOT a Pink-browed Rosefinch. If you draw any white eyebrow stripe, any dark eye-mask, any gray cheek patch, any robust bill, or any red extending over the back of the head, the output is WRONG.\n\nCRITICAL STYLE OVERRIDE (most important instruction \u2014 overrides anything in the main prompt body): This MUST be true kach\u014d-e style. The wings, back, and tail MUST be 2-3 broad FLAT color zones. NO feather-by-feather rendering. NO fine line stippling. NO parallel barb lines. NO gradient shading. The wing is ONE flat brown shape with ONE or TWO thin white wingbar strokes across it \u2014 NOT a feather-diagram. The tail is ONE flat shape with at most ONE accent stroke. The pink breast is ONE flat zone \u2014 not gradiented or modeled. The BODY must be LESS THAN ~30 visible brush strokes TOTAL. If your output has more than ~5 visible internal lines on the wing or back, IT IS WRONG. ABSTRACT the streaking into broad zones, not literal individual streaks. FEWER brush strokes, larger flat zones. Match IMAGE 3 style reference exactly.", + "Accipiter cooperii": "Adult Cooper's Hawk has a SLATE BLUE-GRAY cap and back, NOT a dark indigo or deep blue. The head color is a muted, desaturated gray-blue that transitions gradually to the lighter nape. Avoid heavily saturated dark blue on the head.", + "Accipiter gentilis": "Eurasian Goshawk, ADULT (NOT juvenile). CRITICAL DIAGNOSTIC FEATURES that MUST appear in BOTH poses \u2014 without these the bird is misidentified: (1) ADULT PLUMAGE ONLY \u2014 UNIFORM SLATE-GREY / DARK BLUE-GREY UPPERPARTS (back, folded wings, head crown, nape). DO NOT render brown back, brown wings, or any rufous tones. Brown upperparts = juvenile = WRONG. (2) BOLD WHITE SUPERCILIUM (eyebrow stripe) \u2014 a thick prominent white line running from the bill, above the eye, back to the nape. This must be VERY OBVIOUS, not subtle. Without this stripe the bird looks like a falcon. (3) RED-ORANGE EYE (NOT yellow like a kestrel, NOT dark like a falcon) \u2014 bright reddish-orange iris. (4) DARK SLATE-GRAY CAP and back (NOT solid black hood like a Peregrine Falcon). (5) HEAVILY BARRED WHITE UNDERPARTS with fine wavy gray horizontal bars from throat to belly \u2014 diagnostic, not solid white. (6) LONG ROUNDED TAIL with 3-4 dark bands \u2014 Accipiter shape, NOT pointed like a falcon. (7) STOUT HOOKED YELLOW BEAK with black tip. (8) YELLOW LEGS AND FEET. ANATOMY: BOTH legs and BOTH feet fully visible at the bottom of the body for the perched pose \u2014 neither leg hidden behind the other. EXACTLY one head, one beak, one tail, two wings (folded for perched). THIS IS NOT A FALCON \u2014 Goshawks have rounded wings and long banded tails, falcons have pointed wings and short tails. ABSOLUTELY NO STICK, BRANCH, TWIG, OR PERCH under the feet. The bird floats with toes curled \u2014 the perch is implied only by toe posture. If you draw any horizontal line under the feet, the output is wrong. BOTH poses MUST show the same UNIFORM slate-grey upperparts, white supercilium, red eye, slate cap, and barred white underparts \u2014 same individual ADULT bird.", + "Accipiter striatus": "Sharp-shinned Hawk, adult male. The SMALLEST North American Accipiter \u2014 distinguish from Cooper's Hawk (larger) AND from Old-World species like Japanese Sparrowhawk. DIAGNOSTIC FEATURES that MUST appear in BOTH poses: (1) SMALL HEAD with NO PRONOUNCED CREST or hood \u2014 head looks small and rounded compared to body; (2) DARK SLATE BLUE-GRAY CAP AND BACK (adult male) \u2014 NOT brown like a Japanese Sparrowhawk female; (3) RED-ORANGE EYE in adults (NOT yellow like immatures); (4) RUFOUS-ORANGE BARRING on white breast, extending across entire underparts \u2014 DENSE not sparse; (5) LONG SQUARE-TIPPED TAIL with 3-4 broad dark bands \u2014 the SQUARED tail tip is diagnostic vs. Cooper's Hawk's rounded tail; (6) THIN PENCIL-LIKE LEGS (much thinner than Cooper's Hawk's stout legs) \u2014 yellow color; (7) SHORT HOOKED YELLOW BEAK with black tip. American/North American Accipiter \u2014 NOT a Japanese Sparrowhawk (which has differently shaped tail and different barring pattern). ABSOLUTELY NO STICK, BRANCH, TWIG, OR PERCH. The bird floats with feet gripping air. BOTH poses MUST show identical rufous-barred underparts + slate cap + square tail + red eye \u2014 same individual.", + "Actitis macularius": "Spotted Sandpiper, breeding adult. DIAGNOSTIC FIELD MARKS that MUST appear identically in BOTH perched and flying poses: (1) WHITE BREAST AND BELLY WITH BOLD BLACK ROUND SPOTS \u2014 the eponymous and most distinctive feature, spots should be obvious well-spaced round dots; (2) ALL-BROWN upperparts (back, wings, tail) with NO black areas, distinguishing it from other sandpipers; (3) WHITE SUPERCILIUM (eyebrow stripe) above the eye; (4) ORANGE BILL with a DARK TIP, medium length (NOT very long); (5) SHORT YELLOWISH-ORANGE LEGS (short, not long like other sandpipers); (6) SHORT brown tail with thin white edging; (7) FOR THE FLYING POSE specifically: a distinctive WHITE WINGBAR visible across the wing, white leading and trailing edges to the wings \u2014 stiff-winged low-flight posture. BOTH the perched and flying bird MUST show the IDENTICAL white-with-black-round-spots pattern on the breast and belly. The plumage must look like the SAME individual bird from two angles, not two different birds.", + "Aechmophorus occidentalis": "Western Grebe, breeding adult. DIAGNOSTIC FIELD MARKS that MUST appear identically in BOTH perched and flying poses: (1) BLACK CAP EXTENDS BELOW THE EYE \u2014 the black wraps AROUND the eye, NOT just on top of the head (this is key to distinguish from Clark's Grebe which has white around the eye); (2) RED EYE clearly visible \u2014 the red eye sits within a black mask area; (3) STRAIGHT GREENISH-YELLOW BILL \u2014 long, thin, straight (not upturned like Clark's); (4) LONG WHITE NECK with a sharp boundary where black cap meets white throat; (5) DARK GRAY-BLACK BACK AND UPPERPARTS; (6) DARK FLANKS (not white like Clark's); (7) WHITE UNDERPARTS (belly and breast); (8) FLIGHT: long neck extended fully forward, legs trailing behind, narrow pointed wings. Render with kach\u014d-e simplicity: 2-3 flat color zones (black, white, gray) \u2014 do NOT shade the body internally. BOTH poses MUST show the IDENTICAL black-above-and-below-eye pattern, same greenish-yellow bill, same red eye \u2014 same individual bird.", + "Aegolius acadicus": "Northern Saw-whet Owl, adult. DIAGNOSTIC FIELD MARKS appearing identically in BOTH perched and flying poses: (1) VERY SMALL OWL \u2014 one of the smallest owls in North America (robin-sized); (2) NO EAR TUFTS \u2014 round-headed (this distinguishes from screech-owls); (3) WHITISH FACIAL DISC with brown streaks and a distinctive Y-shaped white pattern between the eyes; (4) LARGE BRIGHT YELLOW EYES; (5) BREAST/BELLY: pale white/cream with broad RUFOUS-BROWN STREAKING (vertical streaks, not bars); (6) UPPERPARTS: rich brown or chestnut-brown with WHITE SPOTS scattered across the back and wings; (7) DARK BILL; (8) BUFFY-FEATHERED LEGS visible at bottom. BOTH poses MUST show the SAME face pattern, same eye color, same streaked breast, same spotted back \u2014 same individual.", + "Aeronautes saxatalis": "White-throated Swift (Aeronautes saxatalis). ABSOLUTE STYLE REQUIREMENT \u2014 kach\u014d-e woodblock print: only 2-4 broad flat color zones on the body, no internal feather-by-feather rendering, no pen-line stippling, no hatching, no fine watercolor texture, no field-guide realism. The entire bird should look painted with ~30 brush strokes total. If you see yourself adding tiny lines or texture across the breast or belly, STOP \u2014 the painted surface must remain FLAT. CRITICAL ANTI-CONTOUR-LINE RULE: do NOT draw any stray curve/contour line tracing the underside of the body. The boundary between the white underparts and the dark sides is a CLEAN flat-color edge \u2014 there must be NO secondary outline running from throat through breast through belly to legs. NO floating curve under the body. NO extra anatomical demarcation. The white belly is a single white shape, the dark sides are a single dark shape, they meet at a clean wash boundary, period. BEAK RULE: tiny black beak with NO whisker line, NO stray dark mark trailing from below or behind the beak, NO gape line. Just a small wedge \u2014 nothing else around the mouth. DIAGNOSTIC PLUMAGE (this is THE field mark \u2014 without it the bird is wrong species): the underparts must show a LONG WHITE CENTRAL STRIPE running from chin straight down the centerline through breast through belly to the vent, flanked by DARK sooty-black sides. This is NOT a Tree Swallow (Tree Swallow has wholly clean white underparts edge-to-edge); the White-throated Swift's white is a CENTRAL VERTICAL STRIPE only \u2014 the sides of the belly, breast and flanks remain SOOTY BLACK. Also two small WHITE FLANK PATCHES near the rump. NO blue back, NO iridescence, NO rufous, NO orange, NO clean horizontal divide between white belly and dark back. POSE 1 (perched): body axis HORIZONTAL like a Koson-print songbird perched on an implied twig, head forward, short stubby tail trailing. Both tiny feet visible just below the belly as small dark marks with toes curled forward \u2014 NOT splayed, NOT clinging vertical, NOT dangling. Body posture is RELAXED and HORIZONTAL. POSE 2 (in flight): both long sickle/scimitar wings fully extended in a curved arc, cigar-shaped body, short stubby blunt tail trailing. Wings are LONG and curved, tail is SHORT. The underside shows the WHITE CENTRAL STRIPE flanked by dark sides \u2014 same plumage pattern as the perched bird. Legs/feet tucked tight against belly with toes folded out of sight, OR extended back along the tail line. No dangling. ANATOMY RULE: exactly two wings, exactly one tail, exactly one head, exactly one beak. If you produce a third wing or duplicate body part, the output is wrong. TAIL: SHORT, STUBBY, square or barely notched \u2014 at MOST 1/3 the body length. DO NOT draw long thin tail streamers or deeply forked tail. If the tail is longer than 1/3 the body length, it is wrong.", + "Agelaius phoeniceus": "Red-winged Blackbird, breeding ADULT MALE. DIAGNOSTIC FIELD MARKS appearing identically in BOTH perched and flying poses: (1) GLOSSY ALL-BLACK BODY throughout (no brown or gray); (2) BRIGHT RED SHOULDER PATCH (epaulet) on each wing \u2014 the eponymous diagnostic feature, vibrant red patch on the upper wing/shoulder; (3) PALE YELLOW WINGBAR directly BELOW the red patch (this red+yellow combination is the signature look) \u2014 the yellow bar is visible at rest, the red is more prominent in flight/display; (4) SHARPLY POINTED BLACK CONICAL BILL; (5) BLACK EYES; (6) BLACK LEGS AND FEET; (7) MEDIUM-LENGTH ROUNDED BLACK TAIL. BOTH poses MUST show the IDENTICAL red shoulder patch with yellow bar below + all-black body \u2014 same individual male.", + "Aix sponsa": "Wood Duck, breeding ADULT MALE. One of the most ornate ducks \u2014 many specific diagnostic features that MUST appear identically in BOTH poses: (1) HEAD: iridescent metallic GREEN with PURPLE highlights, TWO bold WHITE STRIPES on each side (one from base of bill back to the crest, another from behind the eye back to the crest); (2) LONG SWEEPING CREST drooping down the back of the head/neck \u2014 diagnostic shape; (3) BRIGHT RED EYE with RED EYE-RING; (4) MULTICOLORED BILL: red at base, black at tip, with a small white patch on top; (5) WHITE THROAT with TWO FINGER-LIKE WHITE EXTENSIONS \u2014 one curving up onto the cheek, one extending forward as a collar; (6) CHESTNUT-MAROON BREAST with FINE WHITE SPOTS; (7) BUFF/GOLDEN-TAN FLANKS with fine black vermiculation lines; (8) BLACK and WHITE VERTICAL STRIPE between the chestnut breast and buff flank \u2014 a sharp boundary; (9) DARK BACK with iridescent purple-green sheen; (10) SQUARED DARK TAIL; (11) ORANGE-YELLOW LEGS AND WEBBED FEET. BOTH poses MUST show: green head + white stripes + red eye + long crest + chestnut breast with white spots + buff flanks. The bird should be unmistakable. Render with kach\u014d-e simplification \u2014 broad flat zones of green/white/chestnut/buff but with the key color zones intact.", + "Ammodramus savannarum": "Grasshopper Sparrow. THIS IS A PLAIN-FACED SMALL SPARROW with a LARGE PALE PINKISH BILL, FLAT-TOPPED HEAD, and VERY SHORT TAIL. It is NOT Reed Bunting, NOT Vesper Sparrow, NOT Clay-colored Sparrow, NOT Henslow's Sparrow, NOT Savannah Sparrow, NOT Field Sparrow, NOT Song Sparrow, NOT Swamp Sparrow, NOT White-throated Sparrow, NOT House Sparrow. FACE (below eye level): completely PLAIN warm buff/tan wash. NO bold dark eyeline through or behind the eye. NO bold dark malar stripe below the cheek. NO bold pale supercilium (eyebrow). NO bold pale eye-ring (a faint thin one is OK). Only a tiny yellow-orange dot in front of the eye (the lore). The cheek is uniformly soft warm-tan with NO bold marks. If you draw ANY bold dark stripe on the face, the output is WRONG. CROWN (top of head only): finely streaked dark brown with a thin pale buff median stripe. Streaks are VERY SUBTLE \u2014 like 2-3 fine pencil lines, not bold bars. Crown streaks NEVER extend below the eye. BREAST/BELLY: UNIFORM BUFFY-ORANGE wash, NO streaks, NO marks of any kind. BACK: chestnut/black/buff mottling abstracted into 2-3 broad zones, NOT feather-by-feather. KEY DIAGNOSTICS: LARGE PALE PINKISH BILL (stout, conical, prominent). FLAT-TOPPED HEAD. VERY SHORT TAIL. CHUNKY rounded body. PROFILE VIEW preferred for both poses (front/3-quarter view amplifies head markings to wrong-species territory). STYLE OVERRIDE: max 25-30 brush strokes total. Body = 3-4 broad flat color zones. Wings = flat warm-brown zones with at most 1-2 confident dark accent strokes. NO feather-by-feather rendering. NO gradient shading. NO photographic texture. NO stippling. NO cross-hatching. If you produce a striped-faced bunting-like bird, the output is WRONG.", + "Icterus bullockii": "Bullock's Oriole, ADULT BREEDING MALE (Icterus bullockii). THIS IS NOT A BALTIMORE ORIOLE. THIS IS NOT A HOODED ORIOLE. The Bullock's Oriole's defining and ONLY-this-species feature is its ORANGE FACE bisected by a single thin BLACK LINE \u2014 without this orange-face-with-black-line, the bird is misidentified. Render the BRIGHTEST breeding plumage.\n\nABSOLUTELY CRITICAL HEAD/FACE PATTERN (single most important feature \u2014 get this wrong and the bird is the WRONG SPECIES):\n(1) The FACE \u2014 cheeks, lores, the area around and below the eye, the ear-coverts, and the throat-sides \u2014 is ENTIRELY BRIGHT ORANGE. It is NOT black. It is NOT half-black. The orange covers the whole face.\n(2) Running through the orange face is ONE thin BLACK EYE-LINE only. It starts at the base of the bill, runs straight back THROUGH THE EYE, and continues to the nape. It is a thin line \u2014 like a single brush stroke \u2014 bisecting the orange face. This black eye-line is THE diagnostic field mark of Bullock's Oriole.\n(3) The TOP of the head (forehead + crown only \u2014 NOT the face) is BLACK. The black crown is a CAP on the very top of the head. It does NOT extend down over the eye, does NOT extend over the cheek, does NOT cover the face.\n(4) The CHIN is a small narrow BLACK throat-patch / black bib \u2014 a thin black wedge under the bill on the otherwise orange throat. It is small and narrow \u2014 NOT a wide bib, NOT a full hood, NOT a wraparound throat patch.\n(5) The SIDES OF THE NECK, the cheeks, the ear-coverts, and most of the throat remain BRIGHT ORANGE.\n\nDO NOT DRAW: (a) a solid black hood covering the entire head \u2014 THAT IS A BALTIMORE ORIOLE, WRONG SPECIES; (b) a black face mask with orange cap on top \u2014 THAT IS A HOODED ORIOLE, WRONG SPECIES; (c) any black on the cheek; (d) any black below the eye-line. If your head/face is more than ~25% black by area, the output is WRONG. The head must look mostly ORANGE with a black cap on top, a thin black line through the eye, and a small black throat-wedge \u2014 and that's the only black on the head.\n\nBODY PLUMAGE (both poses must match identically \u2014 same individual):\n(6) UPPER BACK (mantle): SOLID BLACK. The entire upper back between the wings is BLACK, not orange. This must be true in BOTH the perched pose AND the flying pose. If you draw an orange back/mantle, the output is WRONG.\n(7) RUMP and lower back: bright ORANGE.\n(8) UNDERPARTS \u2014 breast, belly, undertail-coverts, sides: BRIGHT ORANGE.\n(9) WINGS: BLACK with one HUGE BOLD WHITE PATCH on the wing coverts (a large solid white block on the shoulder area, NOT thin wingbars \u2014 a single big white shape). Flight feathers below the white patch can show thin white edges, but the dominant feature is the BIG WHITE SHOULDER BLOCK.\n(10) TAIL: BLACK central tail feathers, ORANGE outer tail feathers. Tail is medium length, squared.\n(11) BILL: sharply pointed straight black bill with bluish-gray base on the lower mandible. Pointed cone shape, not decurved.\n(12) LEGS: thin dark gray-black. Eye: dark.\n\nPAIR CONSISTENCY (perched and flying must look like the SAME BIRD):\n- Both poses: orange face, thin black eye-line through eye, black crown-cap on top of head, small black throat-wedge, black upper back/mantle, orange underparts, black wings with big white shoulder patch, black-and-orange tail. IDENTICAL plumage on both \u2014 not two different birds.\n\nSTRICT ANTI-DRIFT \u2014 explicitly NOT:\n- NOT Baltimore Oriole (Icterus galbula) \u2014 Baltimore has an entirely BLACK HOOD covering the whole head, face, and throat. Bullock's has an ORANGE FACE with a black eye-line. If the face is mostly black, output is WRONG.\n- NOT Hooded Oriole (Icterus cucullatus) \u2014 Hooded has an orange/yellow CROWN extending over the head with a BLACK FACE MASK around the bill and throat; decurved bill. Bullock's has a BLACK CROWN with an ORANGE FACE; straight pointed bill.\n- NOT Orchard Oriole, NOT Scott's Oriole, NOT a tanager, NOT a redstart.\n\nCRITICAL STYLE OVERRIDE (overrides anything in the main prompt body): TRUE kach\u014d-e woodblock style. The body MUST be 2-4 broad FLAT color zones (orange, black, white) with SHARP BOUNDARIES. NO feather-by-feather rendering. NO fine line stippling. NO pen-hatching. NO gradient shading. NO field-guide realism. The wing is one flat BLACK shape with one big WHITE BLOCK on the shoulder \u2014 NOT a detailed feather-by-feather wing. The orange body is ONE flat orange zone \u2014 NOT modeled, NOT shaded. The black areas are FLAT BLACK \u2014 NOT textured. ~30 brush strokes total for the entire bird. If your output has more than ~5 internal lines on the wing or back, IT IS WRONG. FEWER brush strokes, larger flat zones, sharper boundaries. Match the IMAGE 3 style reference exactly.\n\nNO sticks, NO branches, NO twigs, NO perches, NO leaves, NO foliage, NO substrate. The bird floats on the cream ground.", + "Melanerpes formicivorus": "Acorn Woodpecker, male breeding plumage (Melanerpes formicivorus) \u2014 the 'clown-faced woodpecker'. Use ONLY these colors: GLOSSY BLACK, BRIGHT WHITE, BRIGHT RED, CREAM (background). NO blue, NO brown, NO green, NO yellow, NO gray, NO rufous, NO orange.\n\nTHE MOST IMPORTANT FEATURE \u2014 WHITE IRIS:\n- The eye is a small WHITE CIRCLE with a tiny BLACK PUPIL DOT inside. Spooky-pale eye.\n- NEVER a solid dark eye. This single feature distinguishes Acorn from ALL other black-white-red woodpeckers (Hairy, Downy, Great Spotted, Pileated, Red-headed \u2014 they all have DARK eyes).\n\nTHE CLOWN FACE (face pattern):\n(1) WHITE FOREHEAD: a small clean WHITE triangular patch directly above the bill, between the bill base and the red cap.\n(2) BRIGHT RED CROWN/CAP: from behind the white forehead back to the nape, the entire top and back of the head is solid bright red.\n(3) CONTINUOUS BLACK MASK: the entire side of the face \u2014 from the bill base through and around the eye, back to the nape, down to enclose the entire cheek, ear-coverts, and side of the throat \u2014 is ENTIRELY SOLID BLACK. The white iris sits INSIDE this black band. THE CHEEK IS ALL BLACK. There must be NO white stripe behind the eye, NO white cheek patch, NO white sub-malar stripe down the side of the neck, NO white below the eye.\n(4) SMALL WHITE THROAT/CHIN: a small isolated WHITE patch on the chin directly below the bill, surrounded on all sides by black.\n(5) BLACK BIB: a solid black horizontal band across the upper chest, immediately below the white throat. Clean edge.\n\nIF YOUR OUTPUT HAS A WHITE STRIPE BELOW THE EYE OR A WHITE CHEEK PATCH BESIDE THE EYE, IT IS WRONG (those features mean Hairy/Downy/Great Spotted/Pileated).\n\nBODY:\n- WHITE BELLY: pure plain white, no streaks, no spots, no scaling, no barring, no red, no yellow \u2014 from below the black bib down to the under-tail area.\n- GLOSSY BLACK BACK: entirely solid black, no white center stripe, no white scapular ovals, no barring.\n- BLACK WINGS (folded perched): solid glossy black, no white wingbars.\n- BLACK TAIL: solid.\n\nPERCHED POSE: side-profile clinging upright. BOTH FEET visible at the bottom of the body, side by side. Two legs drawn \u2014 NEVER only one visible.\n\nFLIGHT POSE: both wings fully spread. Diagnostic features that must be visible:\n- WHITE PRIMARY PATCH: large round WHITE patches at the BASE of the primary feathers on each wing (toward the wing tip) \u2014 like white 'headlights' on the black wings.\n- WHITE RUMP: clear band of pure WHITE between the black back and the black tail base \u2014 visible from above and below.\n- Both legs tucked symmetrically (both legs / not one).\n- Underparts visible: WHITE belly, BLACK bib, WHITE throat, RED cap, WHITE forehead, BLACK mask.\n\nANTI-DRIFT \u2014 NOT these species:\n- NOT Hairy Woodpecker (Dryobates villosus): dark eye, white back stripe, white face stripes. Acorn has none.\n- NOT Downy Woodpecker (Dryobates pubescens): same as Hairy.\n- NOT Great Spotted Woodpecker (Dendrocopos major): white scapular ovals, red vent. Acorn has solid black back + white vent.\n- NOT Pileated Woodpecker (Dryocopus pileatus): has crest. Acorn does not.\n- NOT Red-headed Woodpecker (Melanerpes erythrocephalus): fully red head. Acorn has white forehead + black mask + small white throat.\n- NOT Yellow-bellied Sapsucker (Sphyrapicus varius): white wing covert stripe, yellow belly. Acorn has neither.\n- NOT Black-backed Woodpecker (Picoides arcticus): yellow crown, barred flanks. Acorn has red crown + plain white belly.\n\nSTYLE: True kach\u014d-e flat-zones. ~25-30 brush strokes max. Eye = single white circle + tiny black pupil dot. NO feather rendering, NO stippling, NO gradient, NO shading. Confident outlines only. EXACTLY 2 wings, 1 tail, 1 head, 1 beak, 2 legs (both visible at bottom for perched; both tucked for flying).", + "Setophaga petechia": "Mangrove Yellow Warbler breeding adult male. DIAGNOSTIC FIELD MARKS that MUST appear identically in BOTH perched and flying poses: (1) ENTIRE HEAD CHESTNUT-RED/RUFOUS-RED HOOD (full hood \u2014 crown, face, throat all chestnut-red \u2014 this is THE diagnostic feature, not just a cap); (2) bright lemon-yellow underparts (breast, belly, undertail coverts); (3) WIDE RUSTY-RED/CHESTNUT BREAST STREAKS running down the yellow breast and flanks (the 'petechia' liver-spots); (4) yellowish-olive/golden-green back; (5) olive wings with yellow edges to flight feathers (indistinct yellow wing-band); (6) short thin pointed dark bill; (7) small dark eye (often hidden in the chestnut head); (8) short squarish tail with yellow edges to outer tail feathers. CRITICAL FOR THE IN-FLIGHT POSE \u2014 LEG/FOOT HANDLING: In the flying pose, BOTH LEGS AND FEET MUST BE COMPLETELY HIDDEN \u2014 tucked tight against the yellow belly feathers, fully concealed by belly plumage. Do NOT show any tarsus, do NOT show any toes, do NOT show any claws hanging below the body. The belly contour should be a SMOOTH UNINTERRUPTED YELLOW CURVE from breast to vent, with NO leg or foot shapes poking out. If any leg or foot is visible, the output is WRONG. The legs must be FULLY tucked under the body plumage \u2014 invisible. ZERO leg visibility. ZERO foot visibility. ZERO claw visibility. The flying bird should look like a yellow body with chestnut head and spread wings \u2014 and absolutely nothing protruding below the belly line. PERCHED POSE: feet visible at bottom of body with thin delicate olive-buff tarsi (legs below feathers) about 12% of body height, small slim toes curled gently as if grasping a thin perch \u2014 but NO perch is drawn. Compact warbler shape, rounded head. NOT a regular northern yellow warbler (which has all-yellow head \u2014 this one has a FULL chestnut-red head). NO sticks, branches, perches, leaves, twigs, or substrate.", + "Spizella breweri": "CRITICAL: This is the PLAINEST sparrow in North America - extreme plainness IS the field mark. Render an EXCEPTIONALLY PLAIN small slim sandy gray-brown sparrow. ABSOLUTELY MANDATORY plumage features: (1) plain unmarked gray-brown FACE with NO bold dark malar stripe, NO crisp dark eye-line, NO dark ear-patch outline, NO black chin, NO black bib; (2) conspicuous COMPLETE PALE WHITE EYE-RING encircling the eye on otherwise blank face \u2014 the eye-ring is the most striking feature; (3) finely streaked sandy brown crown \u2014 NO solid rufous/chestnut cap, NO pale median crown stripe, NO black crown stripes; (4) plain PALE WHITISH-GRAY unstreaked breast and belly; (5) two THIN faint pale wing bars only; (6) small pale pinkish conical bill with dark tip; (7) long notched brown tail; (8) pale pinkish legs. \n\nABSOLUTELY CRITICAL FOOT POSTURE OVERRIDE \u2014 this overrides any other foot guidance in the main prompt: Because the perch is intentionally omitted (kach\u014d-e convention with floating bird against cream paper), the feet MUST NOT be drawn in a gripping/clutching/curling posture. The bird must not appear to be 'clutching air'. Instead, draw the feet in ONE of these two ways ONLY: (OPTION A \u2014 STRONGLY PREFERRED): The toes are SIMPLY STRAIGHT and pointed FORWARD-AND-DOWN, flat and uncurled, as if the bird is standing on perfectly flat invisible ground. Each foot shows three forward toes laid out FLAT and STRAIGHT, parallel to one another, with NO curving inward, NO hooking, NO grasping shape. The hallux (back toe) points directly back, also straight. The feet look like simple straight lines, almost stick-figure simple. (OPTION B \u2014 ACCEPTABLE): The feet are partially or entirely concealed beneath belly feathers, with only a small portion of the lower tarsi peeking out \u2014 the toes are essentially hidden. ABSOLUTELY FORBIDDEN: NO curled toes, NO grasping shape, NO toes hooking inward, NO talon-like clenched claws, NO bird-clutching-branch posture, NO splayed eagle-grip shape. The toes must look RELAXED and STRAIGHT, not flexed. If in doubt, hide the feet under belly fluff. \n\nSTRICTLY FORBIDDEN PLUMAGE \u2014 do NOT render: NO House Sparrow features, NO Clay-colored Sparrow features (no median crown stripe, no ear-patch outline), NO Chipping Sparrow features (no rufous cap, no white supercilium), NO Field Sparrow features. NO text, NO signature, NO seal, NO watermark, NO perch, NO stick, NO branch, NO leaves, NO substrate of any kind.", + "Larus heermanni": "Heermann's Gull, breeding adult (Larus heermanni). ABSOLUTE COLOR RULE - EVERY pixel of the body is DARK CHARCOAL SLATE-GRAY. Head: small WHITE HOOD (only the head and upper neck are white, like a snug white cap). Everything else - neck-sides, throat, breast, belly, flanks, back, mantle, rump, folded wings, upper-wing surfaces, under-wing linings - is UNIFORM DARK CHARCOAL SLATE-GRAY. THE BELLY IS DARK GRAY, NOT WHITE. There is NO light gray, NO medium gray, NO pale gray - only the dark slate. Wingtips are SOLID BLACK. Tail is SOLID BLACK with a THIN WHITE TIP. Bill is VERMILION RED with SHARP BLACK TIP. Legs/feet JET BLACK. Eye small black dot. ANTI-DRIFT: if more than the small white head-hood is white, the bird is WRONG. NOT Audouin's Gull (which has white belly), NOT Ring-billed Gull (white body, pale gray wings - completely wrong), NOT Western Gull, NOT California Gull, NOT Laughing Gull. The unique silhouette: SMALL WHITE HOOD on top, EVERYTHING ELSE DARK SLATE GRAY. In flight: BOTH wings dark slate above and below (no white wing-linings), with black primaries and a thin white trailing edge at the very tips of the secondaries. Body underside also dark slate. The entire bird in flight should read as a DARK SHAPE except for the white head. ~30 brush strokes total, broad flat zones, NO feather-by-feather rendering.", + "Setophaga townsendi": "Townsend's Warbler, breeding adult male (Setophaga townsendi). DIAGNOSTIC FEATURES that MUST appear in BOTH poses: (1) BRIGHT YELLOW FACE with bold BLACK EAR PATCH (auricular mask) covering the cheek from behind the eye back; (2) BLACK CROWN (entire top of head); (3) BLACK THROAT-BIB extending down to upper breast; (4) BRIGHT YELLOW UPPER BREAST below the black bib; (5) WHITE BELLY with BLACK FLANK STREAKS; (6) OLIVE-GREEN BACK with subtle dark streaks; (7) DARK WINGS with TWO BOLD WHITE WINGBARS; (8) DARK TAIL with white outer tail feathers. FLIGHT-POSE ANATOMY OVERRIDE (most important - past attempts produced crumpled wings): BOTH wings MUST be FULLY VISIBLE, FULLY EXTENDED, SYMMETRIC - one wing on each side of the body, both spread wide in a clean songbird flight silhouette like a swallow or warbler in level flight. NO folded wing, NO crumpled wing, NO ambiguous wing-blob, NO third wing, NO partially-painted wing. EXACTLY two wings, each fully drawn from shoulder to wingtip with primaries clearly visible. The body is horizontal/level with the head extended forward, tail trailing behind. The wings ARC slightly downward from the body shoulders. EXACTLY one head, one tail, two wings, two tucked legs. PAIR-MATCH: the flying bird must show the IDENTICAL face pattern (yellow face + black ear-patch + black crown + black throat) as the perched sibling. ~25-30 brush strokes total. BROAD FLAT zones. NO feather-by-feather rendering. NO sticks, branches, perches, leaves.", + "Sturnus vulgaris": "European Starling, ADULT NON-BREEDING / WINTER plumage (Sturnus vulgaris) - heavily spotted form. THE EXISTING RENDER FAILED because it showed too few spots. THIS VERSION MUST SHOW ABUNDANT WHITE SPOTS COVERING THE ENTIRE BODY. Body color: iridescent black with subtle purple-green sheen. WHITE SPOTS: small clean white drops/dots/V-marks PEPPERED DENSELY across the ENTIRE body - head, throat, breast, belly, flanks, back, scapulars, folded wings, and rump. Spots are abundant, not sparse - imagine the body lightly snowed-on with white flecks. EVERY plumage zone shows spots: don't leave the breast or belly clean; cover them with spots too. Each spot is small (about 1/40 of the body) but there should be 40+ spots visible across the bird. Other features: short pointed YELLOW bill, dark eye, pinkish-red SHORT STOUT legs, short squared tail. Wings have warm-brown flight feathers with pale buff fringes (the closed wing shows pale scaling). ANATOMY: stand low on perch (no perch drawn - toes curled forward as if on implied substrate). EXACTLY two legs, both visible at the bottom of the body, side by side. NOT a blackbird (no orange bill, no plain unspotted body). NOT a grackle (short tail, not long). NOT a Spotless Starling (this bird MUST have abundant spots). STYLE: ~30 brush strokes for the body shape + the white spots as small confident dots scattered across. Flat color zones for the body; the spots are the only fine detail. NO twigs, branches, perches, leaves.", + "Numenius americanus": "Largest North American shorebird with EXTREMELY long, slender, gracefully DOWN-curved bill (4.5-8.5 inches long, dark gray-brown with pinkish base on lower mandible); PLAIN warm cinnamon-buff face with NO bold head stripes (unlike Whimbrel); crown a soft warm buff with at most 3-4 tiny dark dashes (NOT dense streaking); large dark eye; neck and breast warm cinnamon-buff with at most 6-8 tiny dark tick marks total on the neck (NOT every-feather streaking) fading to UNMARKED cinnamon belly; back rendered as 2-3 FLAT broad zones of cinnamon-buff and dark brown (NOT feather-by-feather, NOT stippled, NOT mottled with hundreds of tiny strokes); BRIGHT CINNAMON-RUFOUS underwings and wing linings (key in-flight diagnostic, visible as rich rusty wash under the spread wings); cinnamon-buff rump and tail with a few sparse dark bars (not dense barring); long bluish-gray legs. STYLE OVERRIDE \u2014 THIS IS A KACHO-E PRINT, NOT A FIELD-GUIDE PLATE: render the entire body with NO MORE THAN 30 brush strokes total. The body is essentially 2-3 FLAT color zones with sharp boundaries. NO feather-by-feather rendering, NO pen-stippling, NO crosshatching, NO dense brown specks across the wing or breast, NO gradient shading. If you find yourself drawing more than 8 small dark marks anywhere on the body, STOP \u2014 you are violating the style. ANATOMY OVERRIDE \u2014 flight pose MUST show EXACTLY TWO visible wings (one near wing fully painted, one far wing visible behind/above the body as a clear separate shape \u2014 DO NOT hide or omit the far wing). EXACTLY ONE tail. EXACTLY ONE head. NO wispy or ambiguous lines floating behind the head, neck, or body \u2014 every line must be a confident, deliberate, intentional mark. If you produce only one visible wing in the flight pose, the output is wrong. BOTH poses MUST show: the exaggeratedly long down-curved bill, plain unmarked cinnamon-buff face with NO crown stripes, warm cinnamon body wash. Flight pose MUST show bright cinnamon-rufous underwing linings AND both wings. Same individual bird. NOT a Whimbrel \u2014 NO dark crown stripes, NO dark eye-line. NOT a Marbled Godwit \u2014 bill curves DOWN, never up. NOT a Eurasian Curlew \u2014 warmer cinnamon body, no white belly, no white rump. NO twigs, NO sticks, NO branches, NO leaves, NO grass, NO water, NO substrate." +} \ No newline at end of file