Skip to content

Component: generation pipeline

A developer who has to change how stickers are generated, or who reads a log line such as FIGURE_CUT, CONSISTENCY_REGEN or QA_IDENTITY_MISMATCH and wants to know which stage produced it.

  • One job = one selfie + one outfit → 12 PNG stickers (512 px) → one Telegram sticker set. The orchestration is bot/worker.py; the building blocks are in bot/services/.
  • The image model draws sheets (several figures on a green background in one image). Code cuts the sheet into figures, removes the background, adds a white outline and exports. Drawing several figures per request is what keeps the cost and time of a pack low.
  • Three switches select the path: STICKER_RENDER_MODE (composite or figure), LIKENESS_MODE (empty, classic or hybrid) and CONSISTENCY_MODE (on or off). LIKENESS_MODE wins over CONSISTENCY_MODE.
  • Quality control is layered: geometric checks in code (count, edges, straight cuts), identity checks by a cheap vision model, then single-figure regeneration of only the stickers that failed, inside strict count and time budgets.
  • Every AI call goes through one function with provider fallback, rate-limit backoff, a per-job cost ceiling (JOB_MAX_COST_USD) and a usage record in the database.
  • A job gets at most 2 attempts (MAX_ATTEMPTS). “No funds” at the provider and the cost ceiling fail the job at once, without a retry.
Term Meaning
Sheet One generated image holding several figures (or heads) in a grid on a chroma-key green background (#00FF00)
Figure One person drawn chest-up or as a close-up — one future sticker
Appearance card (“passport” in the code and logs) A strict JSON description of the guest’s look (hair, facial hair, skin tone, age band, face box) produced by one vision call from the selfie. Cached in jobs.appearance_json for the lifetime of the job
Anchor A neutral front-facing portrait of the guest as a character in the chosen art style and outfit. Used as the main reference image for all sheets in consistency mode
Lock A block of prompt text that pins a property: IDENTITY LOCK, HARD LOCKS, COLOR LOCK, SKIN LOCK, OUTFIT LOCK
ΔE Colour distance in CIE LAB (CIE76). Used to compare skin and hair tones
Die-cut outline The white sticker border drawn by code around the whole silhouette
flowchart TB
    claim["Worker slot claims a queued job"]
    card["Appearance card - one vision call, cached on the job"]
    mode{"STICKER_RENDER_MODE"}
    comp["Composite - 2 sheets of 6 heads, pasted on a body template"]
    crop["Face crop from the selfie"]
    lk{"LIKENESS_MODE set"}
    cons{"CONSISTENCY_MODE on"}
    classic["Classic or hybrid path - selfie is the only face source"]
    anchor["Anchor portrait, verified against the selfie"]
    consistent["Consistency path - all sheets in parallel with the anchor"]
    legacy["Legacy figure path - 2 sheets in sequence"]
    cut["Cut sheets into figures, geometric checks"]
    post["Colour post-processing and export to 512 px"]
    qa["Pack quality check by a vision model"]
    regen["Regenerate single figures within budget"]
    pack["createNewStickerSet with all 12 stickers"]
    deliv["Job moves to delivering, slot is free"]
    fallback["Figure sheet never became valid"]

    claim --> card --> mode
    mode -- "composite" --> comp
    mode -- "figure" --> crop --> lk
    lk -- "classic or hybrid" --> classic
    lk -- "empty" --> cons
    cons -- "yes" --> anchor
    anchor -- "ok" --> consistent
    anchor -- "failed" --> legacy
    cons -- "no" --> legacy
    classic --> cut
    consistent --> cut
    legacy --> cut
    cut --> post --> qa --> regen --> pack
    cut -. "FigureSheetInvalid" .-> fallback
    fallback -. "FIGURE_FALLBACK_ENABLED" .-> comp
    comp --> pack
    pack --> deliv

The mode actually used is written to jobs.render_mode: figure, composite, or composite_fallback (figure was requested, but the job was finished in composite).

STICKER_RENDER_MODE LIKENESS_MODE CONSISTENCY_MODE Function in worker.py Face source How sameness across 12 stickers is achieved
composite (code default) ignored ignored build_stickers (tail part) Selfie; sheet 2 also gets sheet 1 Sheets run in sequence; identity check per sheet
figure classic ignored build_figure_stickers_classic Selfie + face crop only; no anchor Sheet 1 first, the other sheets in parallel with sheet 1 as reference; identity check per sheet against the selfie
figure hybrid ignored build_figure_stickers_classic(hybrid=True) Same as classic By code: the same COLOR LOCK text in every prompt, then skin and hair of every figure are shifted to a pack-wide target, green spill removed; one pack-level check against the selfie
figure empty 1 (default) make_anchor → build_figure_stickers_consistent Anchor is image 1, selfie image 2 Anchor + HARD LOCKS in every prompt; all sheets in parallel; skin normalised to the anchor; one pack-level check against the anchor
figure empty 0, or the anchor could not be generated build_figure_stickers (tail part) Selfie + face crop; sheet 2 gets sheet 1 Two 3×2 sheets in sequence; identity check per sheet

Notes:

  • The code default of STICKER_RENDER_MODE is composite, used when the key is absent. The shipped bot/.env.example sets figure. A key that is present but empty or misspelled is a start-up error, not a silent default.
  • Why classic exists (comment block in worker.py): with an anchor, the face is first averaged into a 1K AI portrait and all 12 stickers then copy that portrait, which cost likeness. classic removes the anchor; hybrid keeps that likeness and restores colour consistency in code instead of through the model.
  • The art style (ART_STYLE, stored per job in jobs.art_style) only changes prompt text. Known ids: realistic_illustration, comic_popart, stylized_3d, cute_cartoon (default).

SHEET_LAYOUT applies to the consistency and classic/hybrid paths. Sticker number = pose number (1–12); poses and their emoji come from FIGURE_POSES_SHEET_1 + FIGURE_POSES_SHEET_2 in the catalogue.

Layout Sheets Grid per sheet Aspect Sticker numbers per sheet
3x4 (default) 3 2 × 2 1:1 (1, 2, 4, 5), (3, 7, 6, 10), (8, 9, 11, 12)
2x6 2 3 × 2 FIGURE_SHEET_ASPECT (2:3 or 3:2) (1–6), (7–12)

In every sheet the top row is framed chest-up (hand gestures visible) and the bottom row is a close-up (head and shoulders): the frame for sticker n is FIGURE_FRAMES[(n − 1) mod 6]. Why 3x4 is the default (comment in config.py): cells are larger and margins wider, which halved the number of figures touching a sheet edge and the regenerations they cause.

The composite path and the legacy figure path do not use SHEET_LAYOUT: they always make two sheets (2 × 3 heads, or 3 × 2 figures).

Sheets are requested at Gemini image size 2K; the anchor and single-figure regenerations at 1K (faster and cheaper). A blank grid image (the layout guide) is sent with every figure sheet request so the model keeps to the grid.

# Stage What happens Usage stage label Main code
1 Pre-checks Selfie file must exist; the template id must be in the catalogue. Either failure is a PermanentJobError — no retry — build_stickers, resolve_template
2 Appearance card One vision call → strict JSON validated by pydantic; one retry; on a second failure the job continues without a card passport appearance.build_card
3 Face crop A crop of the selfie around the face box from the card, saved in the session folder. Skipped if the box is missing, tiny or covers the whole frame — appearance.face_crop
4 Anchor (consistency path) One 1K portrait → verification against the selfie (hair length, hair colour, facial hair, glasses) → at most one redraw listing the mismatches → skin shifted towards the selfie estimate → skin colour measured for the locks anchor, anchor_verify make_anchor, consistency.verify_anchor
5 Sheets One image request per sheet with the prompt built from poses, framing, locks, outfit and art style sheet (figure), heads (composite) generate_sheet_figures, ai_pipeline.build_figure_prompt
6 Cutting Chroma key on a reduced copy → connected components → one figure per grid cell; merges detached parts, splits glued neighbours, flags figures cut by an edge — sticker_builder.figures_from_sheet
7 Colour post-processing Consistency: skin of each figure shifted towards the anchor. Hybrid: green spill removed, skin and hair shifted towards the pack target. Classic: nothing — skin_tone.normalize, likeness.postprocess
8 Export Auto-crop → white die-cut outline → fit into 512 × 512 with a 16 px margin → PNG ≤ 512 KB (palette quantisation if needed) — sticker_builder.finish_figure, export_for_telegram
9 Pack check Consistency: anchor + 12 stickers as contact grids → JSON list of outliers. Hybrid: the same against the selfie. Classic: done per sheet in stage 5 qa_pack, qa_sheet consistency.check_pack, likeness.check_pack_selfie, appearance.check_sheet
10 Single-figure regeneration Only the flagged stickers are redrawn, one request each, in parallel; each result is re-checked before it replaces the original regen, qa_regen _regen_outliers, _regen_one, _regen_one_classic
11 Sticker set Name pack<N>_<telegram_user_id>_by_<bot_username>, title built from the guest’s username; all 12 stickers in one createNewStickerSet call, so a set can never be half-built — telegram_stickers.create_pack
12 Hand-over Selfie file deleted at once; job → delivering; verify task spawned — _process_one_job, delivery.spawn
Check Rule Consequence
Figure count Each grid cell must contain exactly one main figure. A second group ≥ 50 % of the main one is a duplicate; a cell whose main group is under 3 % of the median is empty The sheet is invalid → next sheet attempt. In the consistency and classic/hybrid paths, empty cells are tolerated when at least half the figures were found: those stickers are drawn individually
Edge contact A figure touching the sheet edge is cut off Flagged for single-figure regeneration (or a sheet retry in the legacy path)
Close-up bottom edge A close-up touching only the bottom edge Accepted: a flat bust cut that the outline covers (FIGURE_BOTTOM_CUT in the log)
Torso-only contact (EDGE_ALLOW_TORSO_CROP=1) Contact below the chin with no skin at the contact points Accepted (EDGE_TORSO_OK)
Glued neighbours Two figures drawn touching each other are split at the narrowest place near the cell border If the split line is long (> 12 % of the border) the figure counts as cut
Flat side cut (EDGE_FLAT_CUT_CHECK=1) A perfectly straight vertical run on the finished sticker’s contour (an arm cut by a neighbour) Flagged for regeneration; a regenerated figure with the same defect is rejected
Residual edge cut A flagged figure that was not replaced: cut depth is measured ≤ 5 % of the figure height is covered by the outline (EDGE_PADDED); deeper is logged as EDGE_CUT_LEFT and shipped as is
Check Compares Decides
Sheet check (appearance.check_sheet) Selfie (+ face crop) with one sheet, per figure: hairstyle, facial hair by grade, skin tone, same person; for figures also hands matching the face tone and the same outfit Pass/fail per figure is computed in code from the model’s JSON (head_failures)
Anchor verification (consistency.verify_anchor) Selfie with the anchor: hair length, hair colour, facial hair, glasses One anchor redraw at most. Timeout ANCHOR_VERIFY_TIMEOUT_SEC; if the check is unavailable the anchor is used as is
Pack check by anchor (consistency.check_pack) Anchor with all 12 stickers A list of outlier stickers with reasons
Pack check by selfie (likeness.check_pack_selfie) Selfie with all 12 stickers, and the stickers among themselves A list of outlier stickers with reasons

QA_IDENTITY_ENABLED=false switches all vision checks off. A check that is unavailable (provider error) never fails the job: the pack is accepted as generated.

Limit Value Where Why
Job attempts 2 (MAX_ATTEMPTS), 10 s pause before requeue worker.py A second try covers a provider hiccup; more would multiply cost
Head sheet attempts (composite) 2 (SHEET_ATTEMPTS); best cell is kept across attempts worker.py —
Figure sheet attempts FIGURE_SHEET_ATTEMPTS (default 3) for an invalid sheet; a valid sheet is regenerated at most once, and only for a failed check or edge contact that the path does not fix per figure _sheet_figure_attempts A whole-sheet retry costs a full image; single figures are cheaper
Figure → composite fallback Once per job, if FIGURE_FALLBACK_ENABLED=1 build_stickers The guest still gets a pack; the admin gets an informational notice
Appearance card attempts 2 appearance.py Then continue without a card
Anchor redraw 1 make_anchor —
Single-figure regenerations per pack CONSISTENCY_MAX_REGEN (default 4), one try each, in parallel. Missing figures are outside this cap _regen_outliers Bounded extra cost and time
Time budget per regeneration Edge-like defects: EDGE_REGEN_TIMEOUT_SEC (35 s). Check outliers: CONSISTENCY_REGEN_BUDGET_SEC (20 s). Missing figures: no extra budget, only the provider timeout _regen_outliers A visible cut is worth waiting longer for; a missing sticker is mandatory
Regeneration order Missing first, then edge-like, then outliers with the most reasons _regen_outliers The most visible defects are fixed first
Missing figure not redrawn The job fails this attempt with GenerationError and takes the normal retry both builders A pack must have all 12
Parallel image requests AI_MAX_PARALLEL_IMAGES (default 9) per process, across all slots ai_pipeline.image_slot Keeps the burst under the provider’s rate limit
Parallel sheet cuts CUT_MAX_PARALLEL (default: CPU count) per process limits.slot("cut", …) Cutting is CPU- and memory-heavy
Provider timeout AI_TIMEOUT_SEC (default 180) + 15 s safety margin for images; 45 s + 10 s for vision ai_pipeline.py —
Rate-limit backoff Up to 3 waits on the same provider: 5 s, 15 s, 30 s (or the provider’s hint, capped at 60 s) plus jitter. A daily-quota error is not waited for with_rate_limit_backoff Without it a single 429 would fail the sheet and then the job
Cost ceiling per job JOB_MAX_COST_USD (default 1.5), counted across attempts since the job was created or last retried by an admin; checked before every AI call usage.check_budget A stuck job cannot burn the budget
Skin shift Applied only beyond a ΔE threshold (CONSISTENCY_SKIN_DE_MAX, default 6); strength CONSISTENCY_SKIN_STRENGTH (0.85); never longer than SKIN_NORM_MAX_DE (ΔE 4) skin_tone.normalize A larger shift produced visible patches on very light skin

ai_pipeline.generate_head_sheet (images) and ai_pipeline.vision_json (JSON answers) try PRIMARY_AI_PROVIDER first, then FALLBACK_AI_PROVIDER. A provider without an API key is skipped. The SDKs are synchronous, so each call runs in a daemon thread: the event loop stays responsive, and a request in flight does not hold up shutdown.

Default models (overridable without a deploy through GEMINI_IMAGE_MODEL, OPENAI_IMAGE_MODEL, GEMINI_VISION_MODEL, OPENAI_VISION_MODEL): gemini-3.1-flash-image and gpt-image-2 for images, gemini-3.5-flash-lite and gpt-5-mini for vision.

Failure Exception Effect on the job
Primary failed, fallback succeeded — Job continues; the admin chat gets a fallback alert (once per 10 min per provider pair)
All providers failed GenerationError Attempt fails → requeue, or failed after the second attempt
A provider answered “no funds” (HTTP 402 or a billing marker in the text) and no provider succeeded usage.BillingExhausted failed immediately; maintenance pause is switched on for new jobs; the guest gets SERVICE_UNAVAILABLE text (the photo is kept so an admin can retry)
Job cost reached the ceiling usage.JobCostExceeded failed immediately, no retry
Selfie or template file missing, unknown template id PermanentJobError failed immediately, no retry
Figure sheet never valid FigureSheetInvalid Composite fallback if enabled, otherwise like GenerationError
Process stopped mid-job CancelledError Job returns to queued — see sticker-bot
Module Responsibility Source
Worker Slots, job lifecycle, path selection, sheet attempt loops, anchor, regeneration orchestration, sticker set creation, error handling and requeue bot/worker.py
AI pipeline Prompt builders for head sheets and figure sheets, art styles, provider runners, fallback chain, rate-limit backoff, daemon-thread execution, usage recording wrapper bot/services/ai_pipeline.py
Appearance The appearance card schema and prompt, IDENTITY LOCK text, age lock, face crop, per-sheet identity check and its pass/fail logic bot/services/appearance.py
Consistency Anchor prompt, HARD LOCKS text, anchor verification, pack check against the anchor (contact grids) bot/services/consistency.py
Likeness Hybrid mode: skin and hair targets (median of the pack, pulled slightly towards the selfie), COLOR LOCK text, hair normalisation, green despill, pack check against the selfie bot/services/likeness.py
Skin tone Deterministic skin mask, LAB measurement, ΔE, bounded normalisation of the whole face zone bot/services/skin_tone.py
Sticker builder Chroma key, sheet cutting for heads and figures, glued-neighbour split, edge and flat-cut detectors, composition on a body template, die-cut outline, 512 px export, layout guide image bot/services/sticker_builder.py
Telegram stickers Pack name and title rules, createNewStickerSet, readiness check (Bot API count + t.me page twice) bot/services/telegram_stickers.py
Usage One record per provider call (including errors), cost estimate from prices in the environment, per-job ceiling, balance estimate, the daily image-request counter in Pacific time bot/services/usage.py
Stage timing STAGE_TIMING and STAGE_SUMMARY log lines per job attempt; log only, no effect on behaviour, no personal data bot/services/stage_timing.py
Limits Named per-process semaphores (images, cut) that log long waits bot/services/limits.py
Catalogue Outfit texts, the 12 poses and emoji, frame types per cell, sheet layouts bot/catalog.py

All of them live under DATA_DIR and are removed by the TTL cleanup. See storage and infrastructure for the full layout.

Where What
sessions/<session_token>/ Selfie, face crop, raw sheets, reference copy of sheet 1, anchor (raw and corrected), regenerated figures, stickers/sticker_<n>.png
debug_sheets/ A copy of every figure sheet attempt with a .json cut diagnosis, the anchor copy shown in the admin panel, and one job<N>_consistency.json or job<N>_likeness.json summary

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.