Cell Generator
A Spore-style cell editor that extends the Petri Dish catalogue — trace a specimen's radial outline, mark a section, pick an adaptation, and use the Gemini image API to grow a brand-new silhouette.
---
.. toc::
Evolve a new specimen
Pick a base specimen from the catalogue and the RadialLineChart traces its outline as a polar curve. Drag the MARK SECTION range to highlight an angular slice of the outline (the orange wedge), choose an adaptation — a flagellum, cilia, an eyespot, spikes, a jaw… — then either:
- Export SVG — download the marked outline as a vector, or
- Generate (Gemini) — send the marked silhouette to Google's **Gemini 2.5
Flash Image** model with an instruction to grow that part at the marked spot. It returns a new solid-black silhouette (works in light *and* dark mode) that lands in My Specimens.
.. admonition::Groundwork :color: blue
This is the first slice of a larger system: generated specimens are saved as playable cells for the upcoming Petri Dish Game (Spore Cell-Stage style), where adaptations unlock new outline sections to evolve. The Gemini call runs server-side (the GEMINI_IMAGE_API_KEY never reaches the browser).
.. exec::docs.cell_silhouette_generator.cell_silhouette_generator :code: false
How it works
The outline trace, section marking, SVG export and the marked-PNG rasterisation all run client-side (canvas ray-casting, same as the Silhouette Tracer). Only the image generation is a server-side Dash callback: it forwards the marked PNG + a prompt describing the chosen part to the Gemini generateContent endpoint and returns the inlineData PNG as a data URL.
Source
```python
File: docs/cell_silhouette_generator/cell_silhouette_generator.py
import base64 import json import os from pathlib import Path
import requests from dash import (dcc, html, clientside_callback, callback, no_update, Input, Output, State, ALL) import dash_mantine_components as dmc from dash_iconify import DashIconify import dash_mui_scheduler as dms
/cell-silhouette-generator — a Spore-Cell-Stage-style EDITOR that extends the
Petri Dish SPECIMEN CATALOG. Pick a base specimen, the RadialLineChart traces
its OUTLINE, you MARK an angular section of that outline, pick an ADAPTATION
(eyes, flagella, spikes, …), then either EXPORT the marked SVG or send it to
the Gemini image API (GEMINI_IMAGE_API_KEY) to generate a brand-new black
silhouette of the organism with that part grafted on at the marked location.
Generated specimens are stored in "MY SPECIMENS" — the groundwork for the
/petri-dish-game player-cell + unlock system.
#
The Gemini call is a SERVER-SIDE callback so the API key never reaches the
browser. Everything else (tracing, marking, rasterizing) is clientside.
ACCENT = "#3399ff" ACCENT2 = "#e8590c" # highlight / marked-section colour _LICENSE = os.environ.get("MUI_X_LICENSE_KEY", "") _GEMINI_MODEL = os.environ.get("GEMINI_IMAGE_MODEL", "gemini-2.5-flash-image")
---- base specimens: the Petri Dish silhouettes (the catalogue we extend) -----
_PETRI = Path(__file__).resolve().parents[2] / "assets" / "petri" _NAMES = { "navicula": "Navicula", "closterium": "Closterium", "spirogyra": "Spirogyra", "volvox": "Volvox", "euglena": "Euglena", "amoeba": "Amoeba proteus", "paramecium": "Paramecium", "stentor": "Stentor", "vorticella": "Vorticella", "actinosphaerium": "Actinosphaerium", "philodina": "Philodina", "daphnia": "Daphnia", "cyclops": "Cyclops", "celegans": "C. elegans", "_generic": "Generic cell", } BASES = [] for _svg in sorted(_PETRI.glob("*.svg")): slug = _svg.stem BASES.append({"slug": slug, "name": _NAMES.get(slug, slug.title()), "src": f"/assets/petri/{slug}.svg"})
---- adaptation parts (Spore-inspired but realistic cell organelles) ----------
desc feeds the Gemini prompt; role/diet are groundwork for the game economy.
PARTS = [ {"id": "flagellum", "label": "Flagellum", "icon": "tabler:wave-sine", "desc": "a single long whip-like flagellum tail for fast propulsion", "role": "speed", "diet": None}, {"id": "cilia", "label": "Cilia", "icon": "tabler:line-dashed", "desc": "a fringe of many short hair-like cilia for steady swimming", "role": "speed", "diet": None}, {"id": "eye", "label": "Eyespot", "icon": "tabler:eye", "desc": "a rounded photoreceptor eyespot bulge for sensing light", "role": "sense", "diet": None}, {"id": "spike", "label": "Spike", "icon": "tabler:triangle", "desc": "sharp defensive spikes / spines projecting from the membrane", "role": "defense", "diet": None}, {"id": "jaw", "label": "Jaw", "icon": "mdi:tooth-outline", "desc": "a toothed predatory jaw / mouth opening for eating other cells", "role": "attack", "diet": "carnivore"}, {"id": "filter", "label": "Filter mouth", "icon": "tabler:circle-dotted", "desc": "a wide filter-feeding mouth groove for grazing on plant matter", "role": "feed", "diet": "herbivore"}, {"id": "proboscis", "label": "Proboscis", "icon": "tabler:needle", "desc": "a slender extendable proboscis feeding tube", "role": "feed", "diet": "omnivore"}, {"id": "pseudopod", "label": "Pseudopod", "icon": "tabler:hand-finger", "desc": "a flowing amoeboid pseudopod arm for crawling and engulfing", "role": "grip", "diet": None}, {"id": "membrane", "label": "Membrane ruffle", "icon": "tabler:wave-saw-tool", "desc": "a rippling undulating membrane ruffle along the edge", "role": "speed", "diet": None}, {"id": "stinger", "label": "Stinger", "icon": "mdi:needle", "desc": "a venomous stinger / nematocyst barb for attack and defense", "role": "attack", "diet": None}, ] _PART_BY_ID = {p["id"]: p for p in PARTS}
TRACE_SX = { "& .MuiChartsRadialGrid-line": {"stroke": "rgba(51, 153, 255, 0.16)"}, "& .MuiChartsRadialAxis-line": {"stroke": "rgba(51, 153, 255, 0.30)"}, "& .MuiChartsRadialAxis-tick": {"stroke": "rgba(51, 153, 255, 0.30)"}, "& text": {"fill": "light-dark(rgba(40, 90, 150, 0.7), rgba(150, 195, 255, 0.78))", "fontFamily": "monospace", "fontSize": "10px"}, }
------------------------------------------------------------- base picker -----
def _base_thumb(i, item): return dmc.UnstyledButton( dmc.Stack( [html.Img(src=item["src"], className="petri-silhouette", style={"width": "100%", "height": 44, "objectFit": "contain"}), dmc.Text(item["name"], size="9px", ta="center", lineClamp=1, style={"width": "100%"})], gap=2, align="center", ), id={"type": "csg-base", "index": i}, p=6, className="pp-thumb", style={"border": "1px solid var(--mantine-color-default-border)", "borderRadius": "8px", "width": "100%", "background": "var(--mantine-color-body)"}, )
def _part_chip(p): return dmc.Chip( dmc.Group([DashIconify(icon=p["icon"], width=14), html.Span(p["label"])], gap=4, wrap="nowrap"), value=p["id"], variant="outline", size="sm", )
_builder = dmc.Paper( [ dmc.Group( [dmc.Text("BASE SPECIMEN", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Badge("editor", color="brand", variant="light", styles={"root": {"fontFamily": "monospace"}})], justify="space-between", mb="xs", ), html.Div( html.Img(id="csg-preview", src=BASES[0]["src"], className="petri-silhouette", style={"maxWidth": "100%", "maxHeight": 150, "objectFit": "contain"}), style={"height": 160, "display": "flex", "alignItems": "center", "justifyContent": "center", "background": "light-dark(radial-gradient(circle at 50% 45%, #ffffff 0%, #eef4fb 70%, #dde8f5 100%)," " radial-gradient(circle at 50% 45%, #16222f 0%, #101a26 70%, #0b121b 100%))", "borderRadius": "12px"}, ), dmc.Text(BASES[0]["name"], id="csg-base-name", fw=700, ta="center", mt=6, mb="xs"),
dmc.Text("ADAPTATION", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.ChipGroup( dmc.Group([_part_chip(p) for p in PARTS], gap=6, mt=4, mb="sm"), id="csg-part", value="flagellum", multiple=False, ),
dmc.Text("MARK SECTION (angle °)", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.RangeSlider(id="csg-section", min=0, max=360, step=5, value=[30, 90], minRange=10, marks=[{"value": v, "label": str(v)} for v in (0, 90, 180, 270, 360)], color="brand", mt=4, mb="sm"),
dmc.Group( [dmc.Button("Export SVG", id="csg-export", leftSection=DashIconify(icon="tabler:download"), variant="default", size="sm"), dmc.Button("Generate (Gemini)", id="csg-generate", leftSection=DashIconify(icon="tabler:sparkles"), color="brand", size="sm")], gap="sm", ), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 320px", "maxWidth": 360}, )
----------------------------------------------------------- radial outline ----
_outline = dmc.Paper( [ dmc.Group( [dmc.Text("RADIAL OUTLINE", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Text("", id="csg-section-readout", size="xs", c="dimmed", style={"fontFamily": "monospace"})], justify="space-between", mb="xs", ), dmc.Text("The specimen as a polar curve. The orange wedge is the section you've marked " "for a new part — export it, or send it to Gemini to grow the adaptation.", size="xs", c="dimmed", mb="xs"), dms.RadialLineChart( id="csg-chart", height=360, licenseKey=_LICENSE, series=[], rotationAxis=[{"data": [0, 360], "min": 0, "max": 360, "tickInterval": [0, 45, 90, 135, 180, 225, 270, 315]}], radiusAxis=[{"min": 0, "max": 100, "position": "none"}], grid={"rotation": True, "radius": True}, hideLegend=True, skipAnimation=True, margin=14, sx=TRACE_SX, ), ], p="md", radius="md", withBorder=True, style={"flex": "1 1 360px", "maxWidth": 500}, )
--------------------------------------------------------------- gemini lab ----
_lab = dmc.Paper( [ dmc.Group( [dmc.Text("GENESIS LAB", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}), dmc.Badge("Gemini 2.5 Flash Image", id="csg-model", color="grape", variant="light", styles={"root": {"fontFamily": "monospace"}})], justify="space-between", mb="xs", ), dmc.Group( [ dmc.Stack( [dmc.Text("MARKED INPUT", size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), html.Div(html.Img(id="csg-marked-preview", style={"maxWidth": "100%", "maxHeight": 150}), style={"width": 160, "height": 160, "display": "flex", "alignItems": "center", "justifyContent": "center", "background": "var(--mantine-color-default)", "borderRadius": "10px"})], gap=4), DashIconify(icon="tabler:arrow-right", width=26, color=ACCENT), dmc.Stack( [dmc.Text("NEW SPECIMEN", size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), dmc.Box( html.Img(id="csg-result", className="petri-silhouette", style={"maxWidth": "100%", "maxHeight": 150}), pos="relative", style={"width": 160, "height": 160, "display": "flex", "alignItems": "center", "justifyContent": "center", "background": "var(--mantine-color-default)", "borderRadius": "10px"}), dmc.Button("Add to My Specimens", id="csg-save", size="xs", variant="light", color="brand", leftSection=DashIconify(icon="tabler:plus"), disabled=True)], gap=4), ], gap="md", align="flex-start", wrap="wrap", ), dmc.Text("", id="csg-status", size="xs", c="dimmed", mt="xs"), dmc.Divider(my="sm"), dmc.Text("MY SPECIMENS", fw=700, size="xs", c="dimmed", style={"letterSpacing": "1px", "fontFamily": "monospace"}), html.Div(id="csg-mine", style={"display": "flex", "gap": "8px", "flexWrap": "wrap", "marginTop": "6px"}), dmc.Text("Generated specimens are saved here (this session). They become playable cells in the " "Petri Dish Game.", id="csg-mine-hint", size="xs", c="dimmed", mt=4), ], p="md", radius="md", withBorder=True, mt="md", )
----------------------------------------------------------------- gallery -----
_gallery = dmc.Paper( [ dmc.Text("SPECIMEN CATALOG", fw=700, c=ACCENT, style={"letterSpacing": "2px", "fontFamily": "monospace"}, mb="xs"), dmc.SimpleGrid(cols={"base": 4, "sm": 6, "md": 8}, spacing="xs", children=[_base_thumb(i, it) for i, it in enumerate(BASES)]), ], p="md", radius="md", withBorder=True, mt="md", )
component = dmc.Stack( [ dmc.Group([_builder, _outline], align="flex-start", gap="xl", wrap="wrap"), _lab, _gallery, dcc.Store(id="csg-bases", data=BASES), dcc.Store(id="csg-parts", data=PARTS), dcc.Store(id="csg-selected", data={"index": 0}), dcc.Store(id="csg-outline"), # {angles, radii, src, name} dcc.Store(id="csg-marked-png"), # base64 png (no prefix) for Gemini dcc.Store(id="csg-result-store"), # raw Gemini output (black-on-white) dcc.Store(id="csg-clean-store"), # cleaned black-on-transparent silhouette dcc.Store(id="csg-mine-store", data=[]), dcc.Download(id="csg-download"), ], gap="md", )
========================== client-side callbacks =============================
1) base gallery click -> selected index
clientside_callback( """ function(clicks) { const ctx = window.dash_clientside.callback_context; const tid = ctx.triggered_id; if (!tid || tid.index == null) { return window.dash_clientside.no_update; } return {index: tid.index}; } """, Output("csg-selected", "data"), Input({"type": "csg-base", "index": ALL}, "n_clicks"), prevent_initial_call=True, )
2) trace the selected base's outline (async: image -> canvas -> ray-cast)
clientside_callback( """ async function(sel, bases) { const nu = window.dash_clientside.no_update; if (!sel || sel.index == null || !bases) { return [nu, nu, nu]; } const item = bases[sel.index]; if (!item) { return [nu, nu, nu]; } const N = 180; const img = new Image(); await new Promise(res => { img.onload = res; img.onerror = res; img.src = item.src; }); const S = 320; const cv = document.createElement('canvas'); cv.width = S; cv.height = S; const cx2 = cv.getContext('2d'); let iw = img.naturalWidth || S, ih = img.naturalHeight || S; const scale = Math.min(S / iw, S / ih) * 0.9, w = iw * scale, h = ih * scale; cx2.drawImage(img, (S - w) / 2, (S - h) / 2, w, h); let data; try { data = cx2.getImageData(0, 0, S, S).data; } catch (e) { return [{angles: [], radii: [], src: item.src, name: item.name}, item.src, item.name]; } const al = (x, y) => (x < 0 || y < 0 || x >= S || y >= S) ? 0 : data[(y * S + x) * 4 + 3]; let cx = 0, cy = 0, nn = 0; for (let y = 0; y < S; y++) for (let x = 0; x < S; x++) if (al(x, y) > 40) { cx += x; cy += y; nn++; } if (!nn) { return [{angles: [], radii: [], src: item.src, name: item.name}, item.src, item.name]; } cx /= nn; cy /= nn; const angles = [], radii = []; for (let k = 0; k < N; k++) { const v = k / N * 360, a = v * Math.PI / 180, dx = Math.sin(a), dy = -Math.cos(a); let last = 0; for (let r = 2; r < S; r += 1) { if (al(Math.round(cx + dx * r), Math.round(cy + dy * r)) > 40) { last = r; } } angles.push(v); radii.push(last); } let mx = 1; for (let i = 0; i < N; i++) { if (radii[i] > mx) { mx = radii[i]; } } const norm = radii.map(r => Math.round(r / mx * 1000) / 10); // light smoothing const out = norm.slice(); for (let i = 0; i < N; i++) { out[i] = (norm[(i - 1 + N) % N] + norm[i] + norm[(i + 1) % N]) / 3; } return [{angles: angles, radii: out, src: item.src, name: item.name}, item.src, item.name]; } """, Output("csg-outline", "data"), Output("csg-preview", "src"), Output("csg-base-name", "children"), Input("csg-selected", "data"), State("csg-bases", "data"), )
3) outline + section -> chart series (outline line + marked wedge) + readout
clientside_callback( """ function(outline, section) { const nu = window.dash_clientside.no_update; if (!outline || !outline.radii || !outline.radii.length) { return [[], nu, '']; } const ang = outline.angles, rad = outline.radii, N = rad.length; let a0 = section ? section[0] : 0, a1 = section ? section[1] : 0; const inSec = v => (a0 <= a1) ? (v >= a0 && v <= a1) : (v >= a0 || v <= a1); const wedge = ang.map((v, i) => inSec(v) ? rad[i] : null); const series = [ {data: rad, closePath: true, showMark: false, area: true, curve: 'catmullRom', color: '#3399ff', label: outline.name || 'outline'}, {data: wedge, closePath: false, showMark: false, area: true, curve: 'catmullRom', color: '#e8590c', label: 'marked section'}, ]; const rot = [{data: ang, min: 0, max: 360, tickInterval: [0, 45, 90, 135, 180, 225, 270, 315]}]; const readout = 'section ' + a0 + '°–' + a1 + '° (' + ((a0 <= a1 ? a1 - a0 : 360 - a0 + a1)) + '° arc)'; return [series, rot, readout]; } """, Output("csg-chart", "series"), Output("csg-chart", "rotationAxis"), Output("csg-section-readout", "children"), Input("csg-outline", "data"), Input("csg-section", "value"), )
4) rasterize the marked image (base silhouette + highlighted wedge) for Gemini,
and build the live "marked input" preview. Recomputes on base/section change.
clientside_callback( """ async function(outline, section, sel, bases) { const nu = window.dash_clientside.no_update; if (!outline || !bases || sel == null) { return [nu, nu]; } const item = bases[sel.index]; if (!item) { return [nu, nu]; } const a0 = section ? section[0] : 0, a1 = section ? section[1] : 0; const img = new Image(); await new Promise(res => { img.onload = res; img.onerror = res; img.src = item.src; }); const S = 384; const cv = document.createElement('canvas'); cv.width = S; cv.height = S; const g = cv.getContext('2d'); // light backdrop so the black silhouette reads in the Gemini input g.fillStyle = '#ffffff'; g.fillRect(0, 0, S, S); let iw = img.naturalWidth || S, ih = img.naturalHeight || S; const sc = Math.min(S / iw, S / ih) * 0.82, w = iw * sc, h = ih * sc; g.drawImage(img, (S - w) / 2, (S - h) / 2, w, h); // highlight wedge over the marked angular section (0=up, clockwise) const cx = S / 2, cy = S / 2, R = S * 0.5; const toRad = d => (d - 90) * Math.PI / 180; // canvas 0=east; our 0=up => -90 g.beginPath(); g.moveTo(cx, cy); const start = toRad(a0), end = toRad(a1 >= a0 ? a1 : a1 + 360); g.arc(cx, cy, R, start, end, false); g.closePath(); g.fillStyle = 'rgba(232,89,12,0.32)'; g.fill(); g.strokeStyle = 'rgba(232,89,12,0.9)'; g.lineWidth = 3; g.stroke(); let url; try { url = cv.toDataURL('image/png'); } catch (e) { return [nu, nu]; } return [url, url.split(',')[1]]; } """, Output("csg-marked-preview", "src"), Output("csg-marked-png", "data"), Input("csg-outline", "data"), Input("csg-section", "value"), State("csg-selected", "data"), State("csg-bases", "data"), )
5) export the marked outline as a downloadable SVG (vector polygon + wedge)
clientside_callback( """ function(n, outline, section) { if (!n || !outline || !outline.radii || !outline.radii.length) { return window.dash_clientside.no_update; } const ang = outline.angles, rad = outline.radii, N = rad.length; const C = 256, K = 230; const pt = i => { const a = ang[i] * Math.PI / 180, r = rad[i] / 100 * K; return [(C + Math.sin(a) * r).toFixed(1), (C - Math.cos(a) * r).toFixed(1)]; }; let d = 'M' + pt(0).join(','); for (let i = 1; i < N; i++) { d += ' L' + pt(i).join(','); } d += ' Z'; // wedge path for the marked section let a0 = section ? section[0] : 0, a1 = section ? section[1] : 0; const inSec = v => (a0 <= a1) ? (v >= a0 && v <= a1) : (v >= a0 || v <= a1); let wd = '', started = false; for (let i = 0; i < N; i++) { if (inSec(ang[i])) { const p = pt(i); wd += (started ? ' L' : 'M' + C + ',' + C + ' L') + p.join(','); started = true; } } if (started) { wd += ' Z'; } const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">' + '<path d="' + d + '" fill="#111111"/>' + (wd ? '<path d="' + wd + '" fill="rgba(232,89,12,0.45)" stroke="#e8590c" stroke-width="2"/>' : '') + '</svg>'; return {content: svg, filename: (outline.name || 'specimen').replace(/\\s+/g, '_') + '_marked.svg', type: 'image/svg+xml'}; } """, Output("csg-download", "data"), Input("csg-export", "n_clicks"), State("csg-outline", "data"), State("csg-section", "value"), prevent_initial_call=True, )
6) GENERATE — SERVER-SIDE so the API key stays on the server. Sends the marked
PNG + the adaptation instruction to Gemini and returns a new black silhouette.
Ask for black-on-WHITE (which the model produces reliably) — the browser then
thresholds the white away to get a clean black-on-transparent, tintable silhouette.
_PROMPT = ( "This image shows a single-celled microorganism as a solid black silhouette on " "a white background, with a translucent ORANGE wedge marking WHERE a new body " "part should grow. Generate a NEW image of the SAME organism as one connected, " "solid BLACK silhouette centered on a plain SOLID WHITE background, with {desc} " "added and integrated naturally into the body at the marked location. Keep the " "rest of the body shape recognisable. Flat 2D top-down scientific icon " "(PhyloPic style): pure black shape on pure white, no orange, no other colour, " "no shading, no gradient, no outline stroke, no text. Fill most of the frame." )
@callback( Output("csg-result-store", "data"), Output("csg-status", "children"), Output("csg-save", "disabled"), Input("csg-generate", "n_clicks"), State("csg-marked-png", "data"), State("csg-part", "value"), State("csg-outline", "data"), State("csg-section", "value"), prevent_initial_call=True, running=[(Output("csg-generate", "loading"), True, False)], ) def _generate(n, png_b64, part_id, outline, section): if not n: return no_update, no_update, no_update key = os.environ.get("GEMINI_IMAGE_API_KEY", "") if not key: return no_update, "⚠ GEMINI_IMAGE_API_KEY is not set on the server.", True if not png_b64: return no_update, "⚠ Pick a base specimen first so there's an outline to adapt.", True part = _PART_BY_ID.get(part_id, PARTS[0]) base_name = (outline or {}).get("name", "cell") prompt = _PROMPT.format(desc=part["desc"]) url = f"https://generativelanguage.googleapis.com/v1beta/models/{_GEMINI_MODEL}:generateContent" body = {"contents": [{"parts": [ {"text": prompt}, {"inline_data": {"mime_type": "image/png", "data": png_b64}}, ]}]} try: r = requests.post(url, params={"key": key}, json=body, timeout=120) except requests.RequestException as exc: return no_update, f"⚠ Network error reaching Gemini: {exc}", True if r.status_code != 200: detail = "" try: detail = r.json().get("error", {}).get("message", "") except Exception: detail = r.text[:160] return no_update, f"⚠ Gemini error {r.status_code}: {detail}", True parts = r.json().get("candidates", [{}])[0].get("content", {}).get("parts", []) img = next((p for p in parts if "inlineData" in p or "inline_data" in p), None) if not img: txt = next((p.get("text", "") for p in parts if "text" in p), "no image returned") return no_update, f"⚠ Gemini returned no image: {txt[:160]}", True d = img.get("inlineData") or img.get("inline_data") mime = d.get("mimeType") or d.get("mime_type") or "image/png" data_url = f"data:{mime};base64,{d['data']}" status = f"✓ Generated {base_name} + {part['label']} ({section[0]}°–{section[1]}°) — cleaning…" return ({"src": data_url, "name": f"{base_name} + {part['label']}", "part": part_id, "role": part["role"], "diet": part["diet"]}, status, False)
6b) CLEAN — turn Gemini's black-on-WHITE output into a black-on-TRANSPARENT
silhouette (threshold luminance -> alpha). This is what makes it tintable
and lets the dark-mode invert filter show it correctly. Runs in the browser.
clientside_callback( """ async function(raw) { const nu = window.dash_clientside.no_update; if (!raw || !raw.src) { return [nu, nu, nu]; } const img = new Image(); await new Promise(res => { img.onload = res; img.onerror = res; img.src = raw.src; }); const S = 512; const cv = document.createElement('canvas'); cv.width = S; cv.height = S; const g = cv.getContext('2d'); let iw = img.naturalWidth || S, ih = img.naturalHeight || S; const sc = Math.min(S / iw, S / ih), w = iw * sc, h = ih * sc; g.drawImage(img, (S - w) / 2, (S - h) / 2, w, h); let im; try { im = g.getImageData(0, 0, S, S); } catch (e) { return [raw.src, raw, '✓ ' + (raw.name || 'specimen') + ' (raw)']; } const px = im.data, T = 150, SOFT = 45; for (let i = 0; i < px.length; i += 4) { const lum = 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2]; const a = lum >= T + SOFT ? 0 : lum <= T - SOFT ? 255 : Math.round(255 * (T + SOFT - lum) / (2 * SOFT)); px[i] = 0; px[i + 1] = 0; px[i + 2] = 0; px[i + 3] = a; } g.putImageData(im, 0, 0); const clean = cv.toDataURL('image/png'); const out = Object.assign({}, raw, {src: clean}); return [clean, out, '✓ ' + (raw.name || 'specimen') + ' — transparent silhouette ready']; } """, Output("csg-result", "src"), Output("csg-clean-store", "data"), Output("csg-status", "children", allow_duplicate=True), Input("csg-result-store", "data"), prevent_initial_call=True, )
7) save generated specimen into MY SPECIMENS
clientside_callback( """ function(n, result, mine) { if (!n || !result) { return window.dash_clientside.no_update; } mine = (mine || []).slice(); mine.push(result); return mine; } """, Output("csg-mine-store", "data"), Input("csg-save", "n_clicks"), State("csg-clean-store", "data"), State("csg-mine-store", "data"), prevent_initial_call=True, )
8) render MY SPECIMENS gallery
clientside_callback( """ function(mine) { const host = document.getElementById('csg-mine'); if (!host) { return window.dash_clientside.no_update; } mine = mine || []; if (!mine.length) { host.innerHTML = '<span style="font-size:11px;color:var(--mantine-color-dimmed)">— none yet —</span>'; return mine.length; } host.innerHTML = mine.map(m => '<div title="' + (m.name || '') + '" style="width:62px;text-align:center">' + '<img class="petri-silhouette" src="' + m.src + '" style="width:54px;height:54px;object-fit:contain;' + 'border:1px solid var(--mantine-color-default-border);border-radius:8px;padding:3px;' + 'background:var(--mantine-color-body)"/>' + '<div style="font-size:8px;line-height:1.1;margin-top:2px;color:var(--mantine-color-dimmed)">' + (m.name || '').slice(0, 16) + '</div></div>' ).join(''); return mine.length; } """, Output("csg-mine-hint", "title"), Input("csg-mine-store", "data"), ) ```
:defaultExpanded: false :withExpandedButton: true
---
*Source: /cell-silhouette-generator*
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /cell-silhouette-generator/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt