Install
openclaw skills install @zmtucker/drivethru-graphic-artistGraphic-artist tasks for Bacon & Co decorations — (1) generate product mockups by compositing a decoration (logo/graphic) onto a blank product photo (deterministic; no model-generated pixels; self-reviewed), (2) make a DTF decoration "production-ready" / "drop the art" — take the real thumbnail, size it to the decoration location, render at 300 DPI, upload the DTF production file, set size + colors, and create a print sample, driving the decoration toward the 'done' state via the drivethru_mcp decoration_* tools, and (3) clean up degraded / AI-generated flat art before production — deterministically snap it back to its true inks, rebuild faded/broken/jagged outlines, and re-render crisp at print size (fixes the "looks fine as a thumbnail, falls apart at 13 inches" problem). Use whenever the user wants to see a logo on a garment, place artwork on a blank, remove an image background (knock a solid color out of flat art, or segment a photographic subject), tune a print's size/position, clean up / fix / "drop for production" a low-quality or AI-generated logo (ghosting, haze, jagged or fading outlines, soft edges), OR make a decoration production-ready / drop art / get a DTF decoration to done.
openclaw skills install @zmtucker/drivethru-graphic-artistTake a blank product photo plus a decoration image and return a composite mockup. Compositing is deterministic image manipulation: Pillow for transform/compose, rembg (U²-Net segmentation — not generative) for background removal and garment bbox detection.
No pixels are ever model-generated. The compositing pipeline never invokes a generative model — only fall back to an image model to create artwork if the user explicitly asks (e.g. "generate a new logo"), and say so first.
But you must review your own output. After composing, you (the model
running this skill) Read the rendered PNG, judge the placement, and
re-compose with corrective deltas if it's off — up to 3 attempts — before
returning it. This is judgment, not generation: it only tunes the same numeric
flags a human would. See Self-review loop below.
knockout_color.py (crisp edges, no halo — see
Background removal); --auto-remove-bg
runs rembg inline and is meant for a photographic decoration, not flat art.full_front, left_chest, right_chest,
full_back, back_yoke, sleeve, front, …Category (hoodie / tee / hat / mug / …) is helpful but optional. If the
user doesn't say, infer it from the image or chat, or omit it to fall back to
the _defaults rules.
Placement rules are ratios against the detected garment bounding box, not
absolute pixels or inches, so a youth tee and an adult tee get visually
matching prints. Each rule has width_ratio, x_center_ratio, y_top_ratio,
rotation_deg, and an optional max_height_ratio (caps print height to fit a
location's height box — e.g. a hoodie full-front print must clear the pocket).
Look-up order: (category, placement) → (_defaults, placement) → error.
The shipped ratios are reconciled to real industry print dimensions — see
references/decoration_spec.md (with the
canonical placement diagram at assets/decoration_guide.png). That's the
ground truth the self-review loop judges against: a chest logo is pocket-sized
(~¼ the width of a full front), a full back equals a full front, etc.
The catalog ships with the skill at assets/placement_rules.json (read-only
starter). When the agent adds or refines rules, an editable copy is created in
the data dir ($MOCKUP_DATA_DIR or ~/.drivethru/mockup) and persists there.
See references/placement_rules_schema.json
for the exact schema.
python3, plus uv on PATH (used to self-bootstrap dependencies).pip install by hand.
Each script ensures its own imports at startup: if Pillow/numpy/rembg
aren't already importable, it builds a cached venv in the data dir
($MOCKUP_DATA_DIR/.venv, default ~/.drivethru/mockup/.venv) with uv and
re-execs into it. Hosts that honor the frontmatter install.uv pre-install
everything and the bootstrap is a no-op; otherwise the first run pays a short
one-time install. See scripts/_bootstrap.py.Pillow + numpy. Everything except segmentation and cleanup
(knockout_color.py, prepare_dtf_production.py, extract_colors.py).
Installs in ~2 s, no model download.scipy + opencv-python-headless (a few tens of MB of
wheels, no model download). Only cleanup_art.py pulls it in — scipy for
the despeckle/close morphology, headless OpenCV for the corner-preserving
vector trace. Works fully offline. (--method raster skips OpenCV and needs
only scipy.)rembg + onnxruntime (~170 MB of wheels plus a
one-time ~170 MB u2net model download on first segmentation). Only
remove_background.py, detect_garment_bbox.py, and compose_mockup.py
pull it in. The model download needs outbound network once; if it's blocked,
bbox detection falls back to the full image frame and --auto-remove-bg
errors — but the color-key path (knockout_color.py) still works offline.| Script | Purpose |
|---|---|
scripts/compose_mockup.py | The workhorse: detect bbox, look up rule, scale/rotate/paste, write PNG, print a JSON receipt. |
scripts/detect_garment_bbox.py | Standalone: print the garment bbox JSON for a blank. |
scripts/knockout_color.py | Flat art: key a solid background color out (logos/line art/decals) → RGBA PNG with clean anti-aliased edges. Color-to-alpha or flood (die-cut); --analyze first to see what each mode removes and get a color-list-backed recommendation. The right tool for a logo on a plate — see Background removal. |
scripts/remove_background.py | Photos: run rembg (U²-Net segmentation) on a photographic subject → RGBA PNG. Wrong tool for flat logos — use knockout_color.py for those. |
scripts/thumbnail_card.py | Composite art/cutout onto a neutral gray or checker card → a legible thumbnail for the DB image field (white art stops vanishing). Human-facing only; never the production file. Optional. |
scripts/edit_placement_rule.py | Schema-validated, atomic mutator for placement_rules.json (show / add / update / remove). |
scripts/cleanup_art.py | Fix degraded / AI-generated flat art (before a drop): snap it to its true inks, rebuild faded/broken/jagged outlines, re-render crisp at print size (corner-preserving vector trace, or --method raster). Deterministic — no model-generated pixels. Writes a *_proof.png (before/after + outline zoom) to Read and self-review. See references/production_cleanup.md. |
scripts/prepare_dtf_production.py | Production-ready: fit the real art (aspect-locked) into a location's print box and stamp it at 300 DPI → print-ready PNG + inch/pixel receipt. |
scripts/extract_colors.py | Production-ready: extract dominant colors from art as #RRGGBB + coverage (feeds decoration_match_colors). |
scripts/upload_production_file.py | Production-ready: POST a local production file to Odoo's /drivethru_mcp/v1/upload route — server-side base64, no chunking, keeps the (large) file out of your token stream. Preferred for a real 300 DPI file when ODOO_MCP_URL + ODOO_MCP_TOKEN are set; when they're absent, stream the file through the decoration_set_image MCP tool instead. Same 20 MB server guard either way. |
python3 scripts/compose_mockup.py \
--blank /path/to/blank.jpg \
--decoration /path/to/logo.png \
--category hoodie \
--placement full_front \
[--auto-remove-bg] \
[--width-delta-pct 0] [--offset-x-pct 0] [--offset-y-pct 0] \
[--rotate-deg 0] \
[--output /path/to/out.png]
The script prints JSON with the detected garment_bbox, the resolved rule,
the applied ratios/deltas, and the output path. Return the PNG to the user
and add one line in human terms ("55% of the garment width, centered on the
chest, no rotation").
Defaults: rules come from the editable data-dir copy if present, else the
bundled starter; output goes to <data dir>/out/<uuid>.png. Override with
--rules / --output.
The ratio rules are a starting guess. For a given garment/decoration pair they can land the print too high, too small, or off-center — and the deterministic pipeline can't notice, because it has no eyes. You do. Do not return the first compose unseen.
After every compose, run this loop before handing anything to the user:
output path from the receipt.Read the output PNG — actually look at the rendered mockup.full_front, small over the pec for left_chest, etc.).--offset-y-pct +8; too small → --width-delta-pct +12).The full review checklist (per-placement targets, critique→delta mapping,
oscillation/no-progress guards, what to tell the user) is in
references/self_review.md, and the targets there
are anchored to real print dimensions in
references/decoration_spec.md. This runs
entirely in-container — you are the reviewer; there is no separate model call.
Mockups are a back-and-forth. When the user says "bigger", "move it up",
"rotate it", layer deltas on top of the previous run's args (e.g.
--width-delta-pct +10, --offset-y-pct -5, --rotate-deg 5). Keep a running
record of the current flags so each turn builds on the last. The full
feedback→flags mapping and how to promote a tuned result into a saved default
are in references/iterative_feedback.md.
Some art that comes in isn't clean vector — it's "fake vector": a logo an image model generated (or someone upscaled from a tiny JPEG) that looks crisp at thumbnail size but is a soft raster full of ghosting, desaturated haze, faded/broken outlines, jagged uneven strokes, and blurry edges. You don't see it in the DB thumbnail; it's glaring once it prints at 13″. Catching and fixing that is part of "dropping for production."
The move is to restore, not regenerate. A human artist wouldn't salvage the
blurry pixels or feed it to an image generator (which would rewrite the
letterforms and text) — they'd rebuild it as the few flat inks it was always
meant to be. scripts/cleanup_art.py does exactly that, deterministically (no
model-generated pixels): snap every pixel to its true ink (dropping the
haze/ghost), despeckle + close each ink mask to rejoin broken outlines, and
re-render each mask crisp at print size with a corner-preserving vector trace
(smooths the jaggies/waviness without rounding letter corners).
python3 scripts/cleanup_art.py --input /tmp/thumb.png \
--inks '#26296B,#A0202C,#FFFFFF' \ # decoration colors[].rgb_hex (best source)
--output /tmp/thumb_clean.png
It writes the cleaned PNG plus a *_proof.png (before/after on gray + dark,
and a zoom on the outline detail where defects hide) and a JSON receipt with a
review_checklist. Read the proof and self-review it — outlines crisp and
uniform? corners still sharp? letterforms unchanged? ghost/haze gone? — then tune
and re-run if needed. Same eyes-on discipline as the mockup
self-review; the customer sees the reviewed result.
This restores intent, it doesn't redesign: it keeps intended style (e.g. hollow
outline letters stay hollow), preserves counters, and can't invent a mark
that's genuinely missing (only faded/broken ones) — flag those for a redraw.
Full procedure, the defect-recognition guide, tuning table, and the
faithful-vs-restyle line: references/production_cleanup.md.
Separate from mockups: when the user gives you a decoration id and asks to
make it production-ready / drop the art / get it to done, you take the
decoration's real thumbnail, size it to its location, render it at 300 DPI,
upload the DTF production file, set the actual size + colors, and create a print
sample — driving the record toward the done state. First inspect the art at
production scale — if it's degraded / AI-generated (ghosting, haze, faded or
jagged outlines), clean it up with cleanup_art.py
(above) before sizing it. This is deterministic image
work (the same no model-generated pixels rule applies) and almost always
applies to decorations whose method is DTF.
You drive it through the drivethru_mcp decoration_* MCP tools
(decoration_get_production_readiness → decoration_get_image →
decoration_set_image → decoration_update_fields → decoration_match_colors →
decoration_create_sample → decoration_set_state) plus the two scripts
prepare_dtf_production.py and extract_colors.py.
How to call these: the
decoration_*tools are MCP tools your host already exposes — invoke each one directly as a tool call, like any other tool available to you. Do not hand-roll an HTTP request to the Odoo host to invoke a tool, and do not go hunting for anODOO_MCP_URL/ODOO_MCP_TOKENto POST against for that: there is no/callREST route, so a 404 there means you invented a URL, not that the MCP is down.Raw HTTP has exactly two legitimate uses here — both for moving image bytes out of your token stream, never for invoking a tool: downloading a
cdn_urlthatdecoration_get_imagereturns for a large/offloaded binary (step 2), and uploading a real production file withupload_production_file.py(step 5, whenODOO_MCP_URL/ODOO_MCP_TOKENare set). Everything else is a tool call.
Start with decoration_get_production_readiness — it returns a blocking_gaps
list mirroring Odoo's own done gate (production file, size, colors, sample,
and a completed linked design). Per-location max print sizes come from the Odoo
decoration_location record, with references/location_dimensions.json
(built from the official spec sheet, references/decoration_spec_sheet.pdf) as
the fallback.
Note: a decoration can't actually reach done without a linked design in the
done state — expect that gap to remain and report it, after completing every
other step and creating the ready sample.
Full step-by-step procedure: references/production_ready.md.
Separate from the interactive workflow: the Mockup Artist Agent runs a
scheduled routine that sweeps Odoo for open decoration requests assigned to
Zach Tucker, and — for each one where both a blank product image and a
decoration image are already attached — generates a mockup and writes it
into the request's mockup_image field. Requests missing an input, or that
already have a mockup, are recorded as skipped and left alone. Rendering is
the same deterministic pipeline (compose_mockup.py + the mandatory
self-review loop); this routine only wraps it
in a per-request loop plus the Odoo read/write plumbing (dotted-path search
across sale.order.decoration_request_ids, attachment listing, base64 write
via decoration_set_image).
Full step-by-step procedure, per-outcome logging, and the exact routine
prompt/cron to paste into the agent's Routines page:
references/mockup_routine.md.
A second scheduled routine — "General Purpose Web image generation
routine" — images the storefront catalog rather than decoration requests.
It sweeps decorated products (product.template with product_type='bacon_item',
preferably in draft storefront.site catalogues) that have no web image or only
one image shared across colors, and for each color composites the product's
linked_decorations graphic onto that color's blank (resolved through
blank_product_tmpl_id), then writes the reviewed, web-sized mockup back to the
color's product.product image (and the template default). Same deterministic
pipeline and mandatory self-review loop; a mockup
is only as good as its blank, so colors whose blank is still a legacy
on-model/low-res shot are skipped for the vendor-sync routine to fix first.
Full step-by-step procedure, the catalog/blank/decoration data path,
per-outcome logging, and the exact routine prompt:
references/web_image_routine.md.
Show the current catalog:
python3 scripts/edit_placement_rule.py show [--category hoodie] [--placement full_front]
Add a new category/placement when one is missing:
python3 scripts/edit_placement_rule.py add tote front \
--width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30
Refine an existing default (e.g. after the user approves a tuned result):
python3 scripts/edit_placement_rule.py update hoodie full_front --width-ratio 0.58
Edits are validated and written atomically to the editable copy in the data dir (seeded from the bundled starter on first edit) — the shipped asset is never mutated.
There are two background removers here because they solve different problems. Pick by what the source is, not by habit:
| The source is… | Use | Why |
|---|---|---|
| Flat art on a solid color — a logo, line art, a decal, a DTF thumbnail on a white plate | knockout_color.py | Keys on the actual color, so edges stay crisp and there's no halo |
| A photographic subject — a real object/garment/person to isolate from a busy scene | remove_background.py (rembg) | U²-Net segmentation finds the salient subject a color key can't |
Do not use rembg (remove_background.py) on a flat logo. rembg is a
salient-object segmentation model built for photos; on flat art it produces a
soft matte that leaves a light halo, and it has no notion of "make this
color transparent," so it can't cleanly knock out a white plate.
python3 scripts/knockout_color.py --input /tmp/logo.jpg --output /tmp/logo.png \
[--mode color-to-alpha|flood] [--color auto|'#RRGGBB'] [--fuzz 0.10] [--feather 2]
The key color defaults to auto (median of the four corners). Two modes:
color-to-alpha (default) — remove the key color everywhere, with
clean anti-aliased edges. Each pixel's alpha becomes proportional to its
distance from the key color and the foreground color is un-multiplied back
out, so a gray edge pixel becomes semi-transparent black instead of an
opaque gray rim. Best for one-color line art you want printed as ink on the
garment (the garment shows through letter counters and open areas).flood — remove only the background connected to the border (a
tolerant flood fill, feathered at the edge). Enclosed regions of the key color
are kept — a white field inside an outline stays white. This is the
"die-cut sticker" look.The choice hinges on one thing: is the key color an ink on this decoration,
or just background? If it prints → flood (keep it). If it's background →
color-to-alpha (knock it out). Don't eyeball-guess, and don't blindly trust the
color list either — run --analyze first and reconcile the two:
python3 scripts/knockout_color.py --input /tmp/thumb.png --analyze \
--expect-colors '#000000' # the decoration's declared inks (colors[].rgb_hex)
Analyze writes nothing and reports:
enclosed_fraction — key-color pixels inside the art (letter counters, a
field inside an outline). This is the number that decides whether the mode even
matters: near-zero → both modes look the same; large → it's a real call;recommended_mode derived from the inks (key color is a declared ink →
flood; not → color-to-alpha), plus warnings.Use the color list as a guide, then verify. The inks are operator-entered and
can be stale or wrong, so treat the recommendation as a strong prior, not a
verdict — when decision_matters is true, Read the actual cutout before
trusting it. Ask the user only when the signal is genuinely ambiguous
(a meaningful enclosed_fraction and the inks don't resolve it, or the inks
contradict what you see) — show both rendered options in the question. When the
inks clearly resolve it and the result looks right, just proceed.
Example — decoration 2878 (Bacon & Co logo): declared inks
[Black], so white is not an ink → analyze recommendscolor-to-alpha, but flags that it removes ~38% of the image (the interior field). Correct call (a one-color black print where the garment shows through), but big enough to eyeball before uploading.
A decoration has two separate image artifacts — don't let one contaminate the other:
image (thumbnail) | dtf_production_png (production file) | |
|---|---|---|
| For | humans browsing the DB | the printer |
| Background | keep one — legibility wins | none — true cutout on transparency |
| Decides "does white print?" | never | its alpha, set from the color list |
So a thumbnail sitting on a white plate tells you nothing about whether that
white should print — that's what --expect-colors is for. Do not assume
thumbnails have any particular background; today they carry whatever was
supplied (a plate, a photo backdrop, or nothing), and migrating them is a slow,
separate effort. Point the knockout at the thumbnail regardless and let the color
list drive the cutout. If a thumbnail is itself hard to see (white art on a white
UI), fix the thumbnail with thumbnail_card.py (composite onto a neutral gray
or checker card) — never by baking a background into the art or the production
file.
Two failure modes this replaces — both from treating flat art as a photo or as a hard threshold:
color-to-alpha removes all the white (interior included)
with no halo; flood keeps the interior on purpose.color-to-alpha ramps those edge pixels'
alpha instead, keeping the border crisp.python3 scripts/remove_background.py --input /tmp/photo.jpg --output /tmp/cut.png
If the input already has meaningful transparency it is copied through unchanged
({"skipped": true}); pass --force to re-run rembg anyway.
cleanup_art.py and Read the proof before
sizing — see Cleaning up degraded art.Read the rendered PNG and
re-compose with deltas if the placement is off, up to 3 attempts. See
Self-review loop.compose_mockup.py locks it automatically; never
hand it raw pixel dimensions that would squash the decoration.