{"id":"b9770460-43ae-45fc-9575-f5fedf5756b6","entityType":"agent","slug":"clawhub-zmtucker-drivethru-graphic-artist","name":"drivethru-graphic-artist","canonicalUrl":"https://www.xpersona.co/agent/clawhub-zmtucker-drivethru-graphic-artist","canonicalPath":"/agent/clawhub-zmtucker-drivethru-graphic-artist","generatedAt":"2026-10-10T08:48:38.793Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":null},"description":"Graphic-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.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.8K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-graphic-artist","sourceUrl":"https://clawhub.ai/zmtucker/drivethru-graphic-artist","homepage":"https://clawhub.ai/zmtucker/skills/drivethru-graphic-artist","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/zmtucker/drivethru-graphic-artist","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/zmtucker/skills/drivethru-graphic-artist","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":65,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"drivethru-graphic-artist technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":null},"stars":null,"forks":null,"downloads":1785,"packageName":null,"latestVersion":"0.18.1","tractionLabel":"1.8K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T02:08:46.601Z","lastCrawledAt":"2026-10-10T02:08:46.601Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T02:08:46.601Z","lastVerifiedAt":null,"highlights":[{"version":"0.18.1","createdAt":"2026-09-24T18:21:37.564Z","changelog":"- Removed the outdated skill-card.md file. - Minor update to SKILL.md contents and version bump to 0.18.1. - No changes to features or core functionality.","fileCount":30,"zipByteSize":5231927},{"version":"0.18.0","createdAt":"2026-09-24T17:54:21.171Z","changelog":"- Improved documentation and updated instructions in SKILL.md. - Removed deprecated file: skill-card.md. - Updated scripts and references for clarity and maintainability.","fileCount":30,"zipByteSize":5231875},{"version":"0.17.0","createdAt":"2026-09-24T14:45:06.105Z","changelog":"- Added new script: scripts/locations.py - Updated placement rules and related logic (assets/placement_rules.json) - Enhanced or refactored vision review and web mockup job scripts - Removed deprecated file: skill-card.md - Documentation and reference updates for new and changed functionalities","fileCount":30,"zipByteSize":5231547},{"version":"0.15.1","createdAt":"2026-09-23T18:12:45.736Z","changelog":"- Added support for \"left_leg\" and \"right_leg\" placement types in mockup generation and placement rules. - Updated placement rules catalog (assets/placement_rules.json) and documentation to reflect new placings for shorts and pants. - Improved vision review and web mockup job documentation. - Removed redundant skill-card.md file.","fileCount":29,"zipByteSize":5225863},{"version":"0.15.0","createdAt":"2026-09-23T17:08:27.454Z","changelog":"drivethru-graphic-artist 0.15.0 - Removed the skill-card.md file. - Updated documentation in SKILL.md and references/web_image_routine.md. - Enhanced or modified the web mockup job script (scripts/web_mockup_job.py). - General improvements to documentation and internal routines.","fileCount":29,"zipByteSize":5224445},{"version":"0.14.4","createdAt":"2026-09-23T16:22:16.027Z","changelog":"- Removed deprecated skill-card.md file. - Updated references, vision review, and web mockup scripts for improved accuracy and maintenance. - Documentation in SKILL.md clarified and refreshed. - Minor internal adjustments to support new file structure and script behavior.","fileCount":29,"zipByteSize":5223589},{"version":"0.14.3","createdAt":"2026-09-23T12:14:18.089Z","changelog":"- Removed obsolete file: skill-card.md. - Documentation updates in SKILL.md and references/web_image_routine.md for improved clarity. - Minor refinements to scripts/web_mockup_job.py for stability or internal consistency.","fileCount":29,"zipByteSize":5222476},{"version":"0.14.2","createdAt":"2026-09-22T22:09:52.628Z","changelog":"drivethru-graphic-artist v0.14.2 - Placement rules catalog (assets/placement_rules.json) updated or refined. - Improvements or updates to mockup scripts: compose_mockup.py, vision_review.py, web_mockup_job.py. - Documentation updates in SKILL.md and references/web_image_routine.md. - skill-card.md removed. - Internal file and rules organization streamlined for clarity and maintainability.","fileCount":29,"zipByteSize":5222223}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-graphic-artist","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq291581evd78xzn1930b0n87bfny:drivethru-graphic-artist` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/zmtucker/drivethru-graphic-artist before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T08:48:38.787Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-zmtucker-drivethru-graphic-artist/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":null},"readme":"Skill: drivethru-graphic-artist\n\nOwner: zmtucker\n\nSummary: Graphic-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.\n\nTags: latest:0.18.1\n\nVersion history:\n\nv0.18.1 | 2026-09-24T18:21:37.564Z | auto\n\n- Removed the outdated skill-card.md file.\n- Minor update to SKILL.md contents and version bump to 0.18.1.\n- No changes to features or core functionality.\n\nv0.18.0 | 2026-09-24T17:54:21.171Z | auto\n\n- Improved documentation and updated instructions in SKILL.md.\n- Removed deprecated file: skill-card.md.\n- Updated scripts and references for clarity and maintainability.\n\nv0.17.0 | 2026-09-24T14:45:06.105Z | auto\n\n- Added new script: scripts/locations.py\n- Updated placement rules and related logic (assets/placement_rules.json)\n- Enhanced or refactored vision review and web mockup job scripts\n- Removed deprecated file: skill-card.md\n- Documentation and reference updates for new and changed functionalities\n\nv0.15.1 | 2026-09-23T18:12:45.736Z | auto\n\n- Added support for \"left_leg\" and \"right_leg\" placement types in mockup generation and placement rules.\n- Updated placement rules catalog (assets/placement_rules.json) and documentation to reflect new placings for shorts and pants.\n- Improved vision review and web mockup job documentation.\n- Removed redundant skill-card.md file.\n\nv0.15.0 | 2026-09-23T17:08:27.454Z | auto\n\ndrivethru-graphic-artist 0.15.0\n\n- Removed the skill-card.md file.\n- Updated documentation in SKILL.md and references/web_image_routine.md.\n- Enhanced or modified the web mockup job script (scripts/web_mockup_job.py).\n- General improvements to documentation and internal routines.\n\nv0.14.4 | 2026-09-23T16:22:16.027Z | auto\n\n- Removed deprecated skill-card.md file.\n- Updated references, vision review, and web mockup scripts for improved accuracy and maintenance.\n- Documentation in SKILL.md clarified and refreshed.\n- Minor internal adjustments to support new file structure and script behavior.\n\nv0.14.3 | 2026-09-23T12:14:18.089Z | auto\n\n- Removed obsolete file: skill-card.md.\n- Documentation updates in SKILL.md and references/web_image_routine.md for improved clarity.\n- Minor refinements to scripts/web_mockup_job.py for stability or internal consistency.\n\nv0.14.2 | 2026-09-22T22:09:52.628Z | auto\n\ndrivethru-graphic-artist v0.14.2\n\n- Placement rules catalog (assets/placement_rules.json) updated or refined.\n- Improvements or updates to mockup scripts: compose_mockup.py, vision_review.py, web_mockup_job.py.\n- Documentation updates in SKILL.md and references/web_image_routine.md.\n- skill-card.md removed.\n- Internal file and rules organization streamlined for clarity and maintainability.\n\nv0.13.1 | 2026-09-22T21:05:50.196Z | auto\n\ndrivethru-graphic-artist 0.13.1\n\n- Added documentation and logic improvements for the web image review routine (web_mockup_job.py).\n- Updated internal references (references/web_image_routine.md).\n- Removed legacy documentation file (skill-card.md).\n- Minor SKILL.md metadata and version bump to 0.13.1.\n\nv0.13.0 | 2026-09-22T19:50:21.467Z | auto\n\n- Introduced a web image vision review system for independent mockup validation, including new scripts `vision_review.py` and `web_mockup_job.py`.\n- Added support for environment variables to configure an external vision API and model selection.\n- Updated `placement_rules.json` and schema to accommodate expanded placement options and clarify left/right meaning (now always the wearer's perspective).\n- Improved documentation in SKILL.md to detail new configuration options and update usage examples.\n- Removed obsolete `skill-card.md`.\n\nv0.12.0 | 2026-09-08T21:17:37.140Z | auto\n\n- Placement rules system updated: edit, show, add, update, and remove actions now handled via `scripts/edit_placement_rule.py`.\n- Placement rules starter catalog (`assets/placement_rules.json`) updated.\n- Documentation improvements in SKILL.md and references/web_image_routine.md.\n- Improved or revised product mockup composition in `scripts/compose_mockup.py`.\n- Obsolete or redundant file `skill-card.md` removed.\n\nv0.11.0 | 2026-09-08T19:58:23.810Z | auto\n\n- Improved reference documentation and image routine details.\n- Refactored and expanded internal documentation for product mockup routines.\n- Clarified the handling and review process for compositing outputs.\n\nv0.10.0 | 2026-09-08T19:45:16.423Z | auto\n\n- Bump version to 0.10.0.\n- Update documentation in SKILL.md for clarity and accuracy.\n- Remove obsolete skill-card.md file.\n- Make refinements to mockup composition and placement logic (scripts/compose_mockup.py).\n- Revise references/web_image_routine.md for consistency with updated workflows.\n\nv0.9.1 | 2026-09-02T19:26:49.142Z | auto\n\ndrivethru-graphic-artist 0.9.1\n\n- Documentation updates and clarifications in SKILL.md and references.\n- Minor improvements to scripts/upload_production_file.py and related documentation.\n- No major changes to interfaces or core behavior.\n\nv0.9.0 | 2026-09-02T18:22:22.117Z | auto\n\ndrivethru-graphic-artist 0.9.0\n\n- Added initial web image routine reference documentation.\n- Updated SKILL.md with latest information and version number.\n- Removed old skill-card.md documentation file.\n\nv0.8.0 | 2026-08-05T15:19:03.520Z | auto\n\n- Added new reference files: decoration_guide.png, decoration_spec_sheet.pdf, and mockup_routine.md for improved decoration and mockup guidance.\n- Removed outdated documentation file: skill-card.md.\n- Documentation updated in SKILL.md to reflect new references and improved process details.\n\nv0.7.0 | 2026-07-16T15:54:06.877Z | auto\n\nv0.7.0 adds degraded logo/art cleanup for production and new dependencies.\n\n- New: Cleanup degraded/AI-generated flat art before production with the new cleanup_art.py script. Restores true ink shapes and edges at print size.\n- Added: scipy and opencv-python-headless as new dependencies for cleanup workflows.\n- Enhanced: Three dependency tiers (light, cleanup, heavy) to optimize installs for different task types.\n- Docs: Expanded SKILL.md to document art cleanup capabilities and usage.\n- New reference: Added production_cleanup.md for best practices on preparing art for clean production output.\n\nv0.6.0 | 2026-07-16T12:39:13.394Z | auto\n\n**v0.6.0 introduces a self-bootstrapping system, split dependency tiers, and new scripts for cleaner background removal and thumbnails.**\n\n- Added auto bootstrapping: all scripts self-manage dependencies with a per-user venv; no manual `pip install` needed.\n- Dependency tiers: now splits into \"light\" (Pillow+numpy) and \"heavy\" (adds rembg/onnxruntime) installs for faster flat-art tasks.\n- Added scripts: `knockout_color.py` for crisp, color-based background removal (ideal for flat logos); `thumbnail_card.py` for gallery-friendly previews.\n- Improved docs: guides users to use `knockout_color.py` for flat art and `remove_background.py` (rembg) for photos, clarifying ideal image prep workflow.\n- All scripts now importable as Python modules and more robust at startup.\n- Now requires both Pillow and numpy for all tasks; rembg/wheels/model only for photo cutout or bbox detection.\n\nv0.5.2 | 2026-07-16T02:14:45.606Z | auto\n\ndrivethru-graphic-artist v0.5.2\n\n- Updated documentation for the `upload_production_file.py` script, clarifying Odoo upload usage and recommended environment variable setup.\n- Improved accuracy and clarity in SKILL.md, especially around production file uploads and streaming alternatives.\n- Removed outdated file `skill-card.md`.\n\nv0.5.1 | 2026-07-15T19:17:07.687Z | auto\n\n- Minor version update to 0.5.1.\n- Removed redundant file: skill-card.md.\n- Documentation improvements and refinements in SKILL.md and references/production_ready.md.\n- Updated scripts/upload_production_file.py for production file upload logic.\n\nv0.5.0 | 2026-07-15T11:58:52.995Z | auto\n\n- Added a new script, scripts/upload_production_file.py, to support direct production file uploads via Odoo API (no chunking; server-side base64).\n- Updated requirements to include the requests Python package.\n- Improved production-ready workflow documentation, including the new upload script in SKILL.md and references.\n- Removed obsolete skill-card.md documentation file.\n\nv0.4.1 | 2026-07-15T10:31:57.712Z | auto\n\ndrivethru-graphic-artist 0.4.1\n\n- Documentation updates in SKILL.md and references/production_ready.md.\n- Removed obsolete skill-card.md file.\n- No functional or interface changes; update is documentation-only.\n\nv0.4.0 | 2026-07-15T01:14:13.615Z | auto\n\nVersion 0.4.0 — Adds DTF production-ready features alongside mockup compositing.\n\n- New support for making DTF decorations \"production-ready\": fit real art to a location's box, render at 300 DPI, extract colors, and generate print-ready outputs.\n- Added scripts: `prepare_dtf_production.py` (production file prep), `extract_colors.py` (dominant colors).\n- New reference files for production/placement sizing.\n- Skill description expanded: now covers both mockup compositing and production art delivery workflows.\n- Removes unused `skill-card.md` file.\n- All mockup and production steps remain deterministic — no model-generated pixels.\n\nv0.3.0 | 2026-07-02T17:48:51.149Z | auto\n\n- Adds reference documentation on industry-standard decoration sizes and placement targets (`references/decoration_spec.md`), aligning self-review with real print specs.\n- Placement rules now support an optional `max_height_ratio` for better control over print areas (e.g. ensuring full-front prints clear hoodie pockets).\n- Placement diagram and canonical dimensions referenced for more accurate visual matching.\n- SKILL.md and associated docs updated to clarify how rule ratios map to true print locations and sizes, and how the self-review loop uses these specs for quality control.\n- General improvements to placement logic documentation and file structure.\n\nv0.2.0 | 2026-07-02T15:26:36.600Z | auto\n\n**Adds a self-review loop that auto-corrects mockup placements before returning.**\n\n- Skill now reviews its own output by visually checking each rendered mockup and auto-adjusting placement (up to 3 attempts) before returning the result.\n- Deterministic compositing pipeline remains unchanged; still no generative AI.\n- Self-review process and auto-correction logic detailed in the new `references/self_review.md`.\n- Minor description and documentation updates to clarify review behavior and workflow.\n- Removed obsolete `skill-card.md` file.\n\nv0.1.0 | 2026-06-22T19:20:31.599Z | auto\n\n- Initial release: deterministic product mockup generation via image compositing (no generative AI).\n- Composites a user-supplied logo or graphic onto a blank product photo (t-shirts, hoodies, hats, mugs, etc.).\n- Automatically detects garment bounding box and scales/places artwork based on a customizable, ratio-based placement-rules catalog.\n- Supports background removal with rembg; standalone scripts provided for compositing, bounding box detection, and catalog editing.\n- Iterative adjustment of placement and sizing; maintains history of previous changes for tuning.\n- Placement rules catalog is editable and persists across sessions; changes do not affect the shipped starter rules.\n\nArchive index:\n\nArchive v0.18.1: 30 files, 5231927 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2400b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_feedback.md (2201b), references/location_dimensions.json (6095b), references/mockup_routine.md (10012b), references/placement_rules_schema.json (2533b), references/production_cleanup.md (9361b), references/production_ready.md (11128b), references/self_review.md (7035b), references/web_image_routine.md (18595b), scripts/_bootstrap.py (4631b), scripts/_paths.py (1998b), scripts/cleanup_art.py (18129b), scripts/compose_mockup.py (23131b), scripts/detect_garment_bbox.py (3208b), scripts/edit_placement_rule.py (11777b), scripts/extract_colors.py (3995b), scripts/knockout_color.py (16869b), scripts/locations.py (4730b), scripts/prepare_dtf_production.py (5835b), scripts/remove_background.py (3357b), scripts/thumbnail_card.py (4403b), scripts/upload_production_file.py (6332b), scripts/vision_review.py (17979b), scripts/web_mockup_job.py (54482b), skill-card.md (2264b), SKILL.md (30559b), _meta.json (144b)\n\nFile v0.18.1:SKILL.md\n\n---\nname: drivethru-graphic-artist\ndescription: Graphic-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.\nversion: 0.18.1\nemoji: 🎨\nmetadata:\n  openclaw:\n    requires:\n      bins: [python3]\n    envVars:\n      MOCKUP_DATA_DIR:\n        required: false\n        description: >\n          Directory for the editable placement-rules catalog and rendered\n          mockup outputs. Defaults to `~/.drivethru/mockup`. The bundled\n          starter catalog (assets/placement_rules.json) is used read-only\n          until the first edit, which seeds an editable copy here.\n      MOCKUP_VISION_API_KEY:\n        required: false\n        description: >\n          API key for the independent vision review in the web-image routine\n          (web_mockup_job.py / vision_review.py). Falls back to\n          OPENROUTER_API_KEY. Without one, mockups are never written.\n      MOCKUP_VISION_MODEL:\n        required: false\n        description: >\n          Vision model slug for the review (default anthropic/claude-sonnet-5\n          on OpenRouter). MOCKUP_VISION_BASE_URL overrides the\n          OpenAI-compatible endpoint (default https://openrouter.ai/api/v1).\n    install:\n      uv:\n        - Pillow>=10.3,<12\n        - numpy>=1.24,<3\n        - rembg>=2.0.56,<3\n        - onnxruntime>=1.18,<2\n        - scipy>=1.10,<2\n        - opencv-python-headless>=4.8,<6\n        - requests>=2.31,<3\n---\n\n# Drivethru Graphic Artist — Product Mockups\n\nTake a **blank** product photo plus a **decoration** image and return a\ncomposite **mockup**. Compositing is deterministic image manipulation: Pillow\nfor transform/compose, [rembg](https://github.com/danielgatis/rembg) (U²-Net\nsegmentation — *not* generative) for background removal and garment bbox\ndetection.\n\n**No pixels are ever model-generated.** The compositing pipeline never invokes\na generative model — only fall back to an image model to *create* artwork if\nthe user *explicitly* asks (e.g. \"generate a new logo\"), and say so first.\n\n**But you must review your own output.** After composing, you (the model\nrunning this skill) `Read` the rendered PNG, judge the placement, and\nre-compose with corrective deltas if it's off — up to 3 attempts — before\nreturning it. This is judgment, not generation: it only tunes the same numeric\nflags a human would. See [Self-review loop](#self-review-loop-required) below.\n\n## The three inputs\n\n1. **Blank** — photo of the product (t-shirt, hoodie, hat, mug, …). Any\n   resolution or crop; the garment's bounding box is detected automatically.\n2. **Decoration** — the logo/graphic to place. PNG with transparency is ideal.\n   For an opaque image, pre-clean it first: a **flat logo on a solid color**\n   should go through `knockout_color.py` (crisp edges, no halo — see\n   [Background removal](#background-removal-flat-art-vs-photos)); `--auto-remove-bg`\n   runs rembg inline and is meant for a *photographic* decoration, not flat art.\n3. **Placement** — where it goes: `full_front`, `left_chest`, `right_chest`,\n   `center_chest`, `left_sleeve`, `right_sleeve`, `left_leg`, `right_leg`\n   (front of that leg on shorts/pants), `full_back`, `back_yoke`, `front`, … (left/right = the **wearer's** — see\n   [Left / right means the WEARER's](#left--right-means-the-wearers)).\n\n**Category** (hoodie / tee / hat / mug / …) is helpful but optional. If the\nuser doesn't say, infer it from the image or chat, or omit it to fall back to\nthe `_defaults` rules.\n\n## How placement works\n\nPlacement rules are **ratios against the detected garment bounding box**, not\nabsolute pixels or inches, so a youth tee and an adult tee get visually\nmatching prints. Each rule has `width_ratio`, `x_center_ratio`, `y_top_ratio`,\n`rotation_deg`, and an optional `max_height_ratio` (caps print height to fit a\nlocation's height box — e.g. a hoodie full-front print must clear the pocket).\nLook-up order: `(category, placement)` → `(_defaults, placement)` → error.\n\nThe shipped ratios are reconciled to real industry print dimensions — see\n[`references/decoration_spec.md`](references/decoration_spec.md) (with the\ncanonical placement diagram at `assets/decoration_guide.png`). That's the\nground truth the self-review loop judges against: a chest logo is pocket-sized\n(~¼ the width of a full front), a full back equals a full front, etc.\n\nThe catalog ships with the skill at `assets/placement_rules.json` (read-only\nstarter). When the agent adds or refines rules, an editable copy is created in\nthe data dir (`$MOCKUP_DATA_DIR` or `~/.drivethru/mockup`) and persists there.\nSee [`references/placement_rules_schema.json`](references/placement_rules_schema.json)\nfor the exact schema.\n\n## Requirements\n\n- `python3`, plus `uv` on PATH (used to self-bootstrap dependencies).\n- **Dependencies install themselves — you never have to `pip install` by hand.**\n  Each script ensures its own imports at startup: if `Pillow`/`numpy`/`rembg`\n  aren't already importable, it builds a cached venv in the data dir\n  (`$MOCKUP_DATA_DIR/.venv`, default `~/.drivethru/mockup/.venv`) with `uv` and\n  re-execs into it. Hosts that honor the frontmatter `install.uv` pre-install\n  everything and the bootstrap is a no-op; otherwise the first run pays a short\n  one-time install. See [`scripts/_bootstrap.py`](scripts/_bootstrap.py).\n- Three dependency tiers, so the common flat-art path stays cheap:\n  - **light** — `Pillow` + `numpy`. Everything except segmentation and cleanup\n    (`knockout_color.py`, `prepare_dtf_production.py`, `extract_colors.py`).\n    Installs in ~2 s, no model download.\n  - **cleanup** — adds `scipy` + `opencv-python-headless` (a few tens of MB of\n    wheels, **no** model download). Only `cleanup_art.py` pulls it in — scipy for\n    the despeckle/close morphology, headless OpenCV for the corner-preserving\n    vector trace. Works fully offline. (`--method raster` skips OpenCV and needs\n    only scipy.)\n  - **heavy** — adds `rembg` + `onnxruntime` (~170 MB of wheels **plus** a\n    one-time ~170 MB `u2net` model download on first segmentation). Only\n    `remove_background.py`, `detect_garment_bbox.py`, and `compose_mockup.py`\n    pull it in. The model download needs outbound network once; if it's blocked,\n    bbox detection falls back to the full image frame and `--auto-remove-bg`\n    errors — but the color-key path (`knockout_color.py`) still works offline.\n\n## Scripts\n\n| Script | Purpose |\n|---|---|\n| `scripts/compose_mockup.py` | The workhorse: detect bbox, look up rule, scale/rotate/paste, write PNG, print a JSON receipt. |\n| `scripts/detect_garment_bbox.py` | Standalone: print the garment bbox JSON for a blank. |\n| `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](#background-removal-flat-art-vs-photos). |\n| `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. |\n| `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. |\n| `scripts/edit_placement_rule.py` | Schema-validated, atomic mutator for `placement_rules.json` (`show` / `add` / `update` / `remove`). |\n| `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`](references/production_cleanup.md). |\n| `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. |\n| `scripts/extract_colors.py` | Production-ready: extract dominant colors from art as `#RRGGBB` + coverage (feeds `decoration_match_colors`). |\n| `scripts/locations.py` | Decoration location name → wearer-side `placement` / `view` / `image_side` (legs included), and which decorations layer onto the front photo. Odoo sends raw names; this is where they are interpreted. |\n| `scripts/web_mockup_job.py` | **Web-image routine, end to end:** plan (`web_mockup_plan`) → fetch → art prep → compose → geometry + vision checks → verified upload to every size variant. JSON out only (plus one live progress line per colour on stderr); reports vision `cost` and placement `tuning`. `run` / `adjust` / `status`. See [`references/web_image_routine.md`](references/web_image_routine.md). |\n| `scripts/vision_review.py` | Independent vision QA of a rendered mockup → `{verdict: yes/no, checks, issues[{fix}], usage{cost_usd}}`. Sends the image to a vision model over HTTP so it never enters the agent's context. Used by `web_mockup_job.py`. |\n| `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. |\n\n## Composing a mockup\n\n```bash\npython3 scripts/compose_mockup.py \\\n    --blank /path/to/blank.jpg \\\n    --decoration /path/to/logo.png \\\n    --category hoodie \\\n    --placement full_front \\\n    [--auto-remove-bg] \\\n    [--width-delta-pct 0] [--offset-x-pct 0] [--offset-y-pct 0] \\\n    [--rotate-deg 0] \\\n    [--output /path/to/out.png]\n```\n\nThe script prints JSON with the detected `garment_bbox`, the resolved `rule`,\nthe `applied` ratios/deltas, and the `output` path. Return the PNG to the user\nand add one line in human terms (\"55% of the garment width, centered on the\nchest, no rotation\").\n\nDefaults: rules come from the editable data-dir copy if present, else the\nbundled starter; output goes to `<data dir>/out/<uuid>.png`. Override with\n`--rules` / `--output`.\n\n## Self-review loop (required)\n\nThe ratio rules are a *starting guess*. For a given garment/decoration pair\nthey can land the print too high, too small, or off-center — and the\ndeterministic pipeline can't notice, because it has no eyes. You do. **Do not\nreturn the first compose unseen.**\n\nAfter every compose, run this loop before handing anything to the user:\n\n1. **Compose** with the current flags; note the `output` path from the receipt.\n2. **`Read` the output PNG** — actually look at the rendered mockup.\n3. **Judge** it against what the placement should look like (centered on the\n   chest for `full_front`, small over the pec for `left_chest`, etc.).\n4. If it looks good → return it. If it's off and you have attempts left →\n   derive corrective deltas and re-compose, **layering them on the previous\n   run's flags** (same deltas as the iterative-feedback table: e.g. print\n   riding too high → `--offset-y-pct +8`; too small → `--width-delta-pct +12`).\n5. **Hard cap: 3 compose attempts.** If attempt 3 still isn't great, return the\n   best one and tell the user in one line what's still off and offer to keep\n   tuning. Never loop past 3, and never ping-pong a delta's sign — if you\n   overshoot, you're close; accept the better result.\n\nThe full review checklist (per-placement targets, critique→delta mapping,\noscillation/no-progress guards, what to tell the user) is in\n[`references/self_review.md`](references/self_review.md), and the targets there\nare anchored to real print dimensions in\n[`references/decoration_spec.md`](references/decoration_spec.md). This runs\nentirely in-container — you are the reviewer; there is no separate model call.\n\n## Iterative feedback\n\nMockups are a back-and-forth. When the user says \"bigger\", \"move it up\",\n\"rotate it\", layer deltas on top of the **previous** run's args (e.g.\n`--width-delta-pct +10`, `--offset-y-pct -5`, `--rotate-deg 5`). Keep a running\nrecord of the current flags so each turn builds on the last. The full\nfeedback→flags mapping and how to promote a tuned result into a saved default\nare in [`references/iterative_feedback.md`](references/iterative_feedback.md).\n\n## Cleaning up degraded / AI-generated art\n\nSome art that comes in isn't clean vector — it's **\"fake vector\":** a logo an\nimage model generated (or someone upscaled from a tiny JPEG) that *looks* crisp\nat thumbnail size but is a soft raster full of **ghosting, desaturated haze,\nfaded/broken outlines, jagged uneven strokes, and blurry edges.** You don't see\nit in the DB thumbnail; it's glaring once it prints at **13″**. Catching and\nfixing that is part of \"dropping for production.\"\n\n**The move is to restore, not regenerate.** A human artist wouldn't salvage the\nblurry pixels or feed it to an image generator (which would rewrite the\nletterforms and text) — they'd rebuild it as the few flat inks it was always\nmeant to be. `scripts/cleanup_art.py` does exactly that, **deterministically (no\nmodel-generated pixels):** snap every pixel to its true ink (dropping the\nhaze/ghost), despeckle + close each ink mask to rejoin broken outlines, and\nre-render each mask crisp at print size with a **corner-preserving vector trace**\n(smooths the jaggies/waviness without rounding letter corners).\n\n```bash\npython3 scripts/cleanup_art.py --input /tmp/thumb.png \\\n    --inks '#26296B,#A0202C,#FFFFFF' \\     # decoration colors[].rgb_hex (best source)\n    --output /tmp/thumb_clean.png\n```\n\nIt writes the cleaned PNG **plus a `*_proof.png`** (before/after on gray + dark,\nand a zoom on the outline detail where defects hide) and a JSON receipt with a\n`review_checklist`. **`Read` the proof and self-review it** — outlines crisp and\nuniform? corners still sharp? letterforms unchanged? ghost/haze gone? — then tune\nand re-run if needed. Same eyes-on discipline as the mockup\n[self-review](references/self_review.md); the customer sees the reviewed result.\n\nThis restores *intent*, it doesn't redesign: it keeps intended style (e.g. hollow\noutline letters stay hollow), preserves counters, and **can't invent a mark\nthat's genuinely missing** (only faded/broken ones) — flag those for a redraw.\nFull procedure, the defect-recognition guide, tuning table, and the\nfaithful-vs-restyle line: **[`references/production_cleanup.md`](references/production_cleanup.md).**\n\n## Making art production-ready (DTF) — \"dropping\" the art\n\nSeparate from mockups: when the user gives you a **decoration id** and asks to\nmake it **production-ready** / **drop the art** / **get it to done**, you take the\ndecoration's real thumbnail, size it to its location, render it at **300 DPI**,\nupload the DTF production file, set the actual size + colors, and create a print\nsample — driving the record toward the `done` state. **First inspect the art at\nproduction scale** — if it's degraded / AI-generated (ghosting, haze, faded or\njagged outlines), clean it up with `cleanup_art.py`\n([above](#cleaning-up-degraded--ai-generated-art)) *before* sizing it. This is deterministic image\nwork (the same *no model-generated pixels* rule applies) and almost always\napplies to decorations whose method is **DTF**.\n\nYou drive it through the `drivethru_mcp` **`decoration_*`** MCP tools\n(`decoration_get_production_readiness` → `decoration_get_image` →\n`decoration_set_image` → `decoration_update_fields` → `decoration_match_colors` →\n`decoration_create_sample` → `decoration_set_state`) plus the two scripts\n`prepare_dtf_production.py` and `extract_colors.py`.\n\n> **How to call these:** the `decoration_*` tools are MCP tools your host\n> already exposes — invoke each one **directly as a tool call**, like any other\n> tool available to you. Do **not** hand-roll an HTTP request to the Odoo host to\n> *invoke* a tool, and do **not** go hunting for an `ODOO_MCP_URL` / `ODOO_MCP_TOKEN`\n> to POST against for that: there is **no `/call` REST route**, so a 404 there\n> means you invented a URL, not that the MCP is down.\n>\n> Raw HTTP has exactly **two** legitimate uses here — both for moving image\n> *bytes* out of your token stream, never for invoking a tool: **downloading** a\n> `cdn_url` that `decoration_get_image` returns for a large/offloaded binary\n> (step 2), and **uploading** a real production file with `upload_production_file.py`\n> (step 5, when `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are set). Everything else is a\n> tool call.\n\nStart with `decoration_get_production_readiness` — it returns a `blocking_gaps`\nlist mirroring Odoo's own `done` gate (production file, size, colors, sample,\nand a completed linked design). Per-location max print sizes come from the Odoo\n`decoration_location` record, with [`references/location_dimensions.json`](references/location_dimensions.json)\n(built from the official spec sheet, `references/decoration_spec_sheet.pdf`) as\nthe fallback.\n\nNote: a decoration can't actually reach `done` without a linked `design` in the\n`done` state — expect that gap to remain and report it, after completing every\nother step and creating the ready sample.\n\n**Full step-by-step procedure: [`references/production_ready.md`](references/production_ready.md).**\n\n## Batch mockup routine (Mockup Artist Agent)\n\nSeparate from the interactive workflow: the **Mockup Artist Agent** runs a\nscheduled routine that sweeps Odoo for open decoration requests **assigned to\nZach Tucker**, and — for each one where both a **blank product image** and a\n**decoration image** are already attached — generates a mockup and writes it\ninto the request's `mockup_image` field. Requests missing an input, or that\nalready have a mockup, are recorded as `skipped` and left alone. Rendering is\nthe same deterministic pipeline (`compose_mockup.py` + the mandatory\n[self-review loop](#self-review-loop-required)); this routine only wraps it\nin a per-request loop plus the Odoo read/write plumbing (dotted-path search\nacross `sale.order.decoration_request_ids`, attachment listing, base64 write\nvia `decoration_set_image`).\n\n**Full step-by-step procedure, per-outcome logging, and the exact routine\nprompt/cron to paste into the agent's Routines page:\n[`references/mockup_routine.md`](references/mockup_routine.md).**\n\n## Web-image routine — per-color storefront mockups (Mockup Artist Agent)\n\nA second scheduled routine — **\"General Purpose Web image generation\nroutine\"** — images the **storefront catalog**: products built in bulk from\nblanks + decorations when a store is created, which arrive with no image. For\neach colour it composites the product's front decoration(s) onto that colour's\nblank and writes the mockup to every size variant of the colour.\n\n**It is one command — `scripts/web_mockup_job.py run --site-id <id>` — and it\nis different from the interactive flow above in three ways that matter:**\n\n- **Do not `Read` or open any image in this routine.** The self-review is done\n  by an independent vision model inside the script (`vision_review.py`), which\n  returns a yes/no verdict as JSON. Images in your context balloon the session.\n- **Do not fetch or write images yourself** — no `decoration_get_image`, no\n  `include_binary`, no `product_write` image fields, no chunked\n  `decoration_set_image`. The script downloads over HTTP and uploads byte-exact\n  through `POST <ODOO_MCP_URL>/upload`, verified.\n- **A \"no\" verdict comes with a fix:** run the job's `next` command\n  (`web_mockup_job.py adjust --job <job> --apply-suggested`). Max 3 attempts.\n\nBlank presence and art choice (embroidery/DTF production PNG before the\nthumbnail, whose background is removed and verified) are resolved server-side\nby the `web_mockup_plan` MCP tool, which returns each decoration's raw location\nname. Wearer-side placement is decided in the skill by `scripts/locations.py`.\n\n**Full procedure, hard rules, statuses, and the exact routine prompt:\n[`references/web_image_routine.md`](references/web_image_routine.md).**\n\n## Left / right means the WEARER's\n\nEvery placement name — `left_chest`, `right_chest`, `left_sleeve`,\n`right_sleeve` — refers to the **wearer's** side. Blanks are flat **front**\nphotos, so the wearer's left is on the **image's right** (`x_center_ratio >\n0.5`) and the wearer's right is on the image's left. The bundled rules follow\nthis; `compose_mockup.py` mirrors (and warns about) any rule that doesn't, and\n`edit_placement_rule.py` refuses to save one. `--offset-x-pct` is always in\nimage coordinates: positive moves toward the image's right. Plain `sleeve` is\nrejected as ambiguous.\n\n## Editing the rules catalog\n\nShow the current catalog:\n\n```bash\npython3 scripts/edit_placement_rule.py show [--category hoodie] [--placement full_front]\n```\n\nAdd a new category/placement when one is missing:\n\n```bash\npython3 scripts/edit_placement_rule.py add tote front \\\n    --width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30\n```\n\nRefine an existing default (e.g. after the user approves a tuned result):\n\n```bash\npython3 scripts/edit_placement_rule.py update hoodie full_front --width-ratio 0.58\n```\n\nEdits are validated and written atomically to the editable copy in the data dir\n(seeded from the bundled starter on first edit) — the shipped asset is never\nmutated.\n\n## Background removal: flat art vs photos\n\nThere are **two** background removers here because they solve different problems.\nPick by what the source *is*, not by habit:\n\n| The source is… | Use | Why |\n|---|---|---|\n| **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 |\n| **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 |\n\n**Do not use rembg (`remove_background.py`) on a flat logo.** rembg is a\nsalient-object segmentation model built for photos; on flat art it produces a\nsoft matte that leaves a light **halo**, and it has no notion of \"make *this*\ncolor transparent,\" so it can't cleanly knock out a white plate.\n\n### Knocking a color out of flat art\n\n```bash\npython3 scripts/knockout_color.py --input /tmp/logo.jpg --output /tmp/logo.png \\\n    [--mode color-to-alpha|flood] [--color auto|'#RRGGBB'] [--fuzz 0.10] [--feather 2]\n```\n\nThe key color defaults to `auto` (median of the four corners). Two modes:\n\n- **`color-to-alpha`** (default) — remove the key color **everywhere**, with\n  clean anti-aliased edges. Each pixel's alpha becomes proportional to its\n  distance from the key color and the foreground color is un-multiplied back\n  out, so a gray edge pixel becomes *semi-transparent black* instead of an\n  opaque gray rim. Best for one-color line art you want printed as ink on the\n  garment (the garment shows through letter counters and open areas).\n- **`flood`** — remove only the background **connected to the border** (a\n  tolerant flood fill, feathered at the edge). Enclosed regions of the key color\n  are **kept** — a white field inside an outline stays white. This is the\n  \"die-cut sticker\" look.\n\n#### Which mode? Decide it, don't guess it\n\nThe choice hinges on one thing: **is the key color an *ink* on this decoration,\nor just background?** If it prints → `flood` (keep it). If it's background →\n`color-to-alpha` (knock it out). Don't eyeball-guess, and don't blindly trust the\ncolor list either — **run `--analyze` first** and reconcile the two:\n\n```bash\npython3 scripts/knockout_color.py --input /tmp/thumb.png --analyze \\\n    --expect-colors '#000000'      # the decoration's declared inks (colors[].rgb_hex)\n```\n\nAnalyze writes nothing and reports:\n- the auto-detected **key color**, and whether it matches a declared ink;\n- **`enclosed_fraction`** — key-color pixels *inside* the art (letter counters, a\n  field inside an outline). This is the number that decides whether the mode even\n  matters: near-zero → both modes look the same; large → it's a real call;\n- a **`recommended_mode`** derived from the inks (key color is a declared ink →\n  `flood`; not → `color-to-alpha`), plus `warnings`.\n\n**Use the color list as a guide, then verify.** The inks are operator-entered and\ncan be stale or wrong, so treat the recommendation as a strong prior, not a\nverdict — when `decision_matters` is true, `Read` the actual cutout before\ntrusting it. Ask the user **only** when the signal is genuinely ambiguous\n(a meaningful `enclosed_fraction` **and** the inks don't resolve it, or the inks\ncontradict what you see) — show both rendered options in the question. When the\ninks clearly resolve it and the result looks right, just proceed.\n\n> Example — decoration 2878 (Bacon & Co logo): declared inks `[Black]`, so white\n> is *not* an ink → analyze recommends `color-to-alpha`, but flags that it removes\n> ~38% of the image (the interior field). Correct call (a one-color black print\n> where the garment shows through), but big enough to eyeball before uploading.\n\n#### The thumbnail's background is not a print instruction\n\nA decoration has two separate image artifacts — don't let one contaminate the\nother:\n\n| | `image` (thumbnail) | `dtf_production_png` (production file) |\n|---|---|---|\n| For | humans browsing the DB | the printer |\n| Background | **keep one** — legibility wins | **none** — true cutout on transparency |\n| Decides \"does white print?\" | never | its alpha, set from the color list |\n\nSo a thumbnail sitting on a white plate tells you **nothing** about whether that\nwhite should print — that's what `--expect-colors` is for. **Do not assume\nthumbnails have any particular background**; today they carry whatever was\nsupplied (a plate, a photo backdrop, or nothing), and migrating them is a slow,\nseparate effort. Point the knockout at the thumbnail regardless and let the color\nlist drive the cutout. If a thumbnail is itself hard to see (white art on a white\nUI), fix the *thumbnail* with `thumbnail_card.py` (composite onto a neutral gray\nor checker card) — never by baking a background into the art or the production\nfile.\n\nTwo failure modes this replaces — both from treating flat art as a photo or as a\nhard threshold:\n\n- *\"Small halo, and white left inside the logo\"* → rembg's soft matte + no\n  interior handling. `color-to-alpha` removes all the white (interior included)\n  with no halo; `flood` keeps the interior on purpose.\n- *\"The black border got all muddied up\"* → a hard \"make white transparent\"\n  threshold leaves the anti-aliased gray edge pixels fully opaque, so the smooth\n  edge turns into a dirty jagged rim. `color-to-alpha` ramps those edge pixels'\n  alpha instead, keeping the border crisp.\n\n### Segmenting a photographic subject (rembg)\n\n```bash\npython3 scripts/remove_background.py --input /tmp/photo.jpg --output /tmp/cut.png\n```\n\nIf the input already has meaningful transparency it is copied through unchanged\n(`{\"skipped\": true}`); pass `--force` to re-run rembg anyway.\n\n## Rules to follow\n\n- **No model-generated pixels** in the compositing *or* cleanup pipeline unless\n  the user explicitly asks — and say so first. Cleaning up degraded art means\n  restoring its existing inks deterministically, **not** regenerating it (a\n  generative model would rewrite the letterforms/text). Reviewing your own output\n  is fine and required — that's judgment, not generation.\n- **Inspect art at production scale before a drop.** A thumbnail hides ghosting,\n  haze, and faded/jagged outlines that wreck a 13″ print. If the art is degraded\n  or AI-generated, clean it with `cleanup_art.py` and `Read` the proof before\n  sizing — see [Cleaning up degraded art](#cleaning-up-degraded--ai-generated-art).\n- **Always self-review before returning.** `Read` the rendered PNG and\n  re-compose with deltas if the placement is off, up to 3 attempts. See\n  [Self-review loop](#self-review-loop-required).\n- **Respect aspect ratio.** `compose_mockup.py` locks it automatically; never\n  hand it raw pixel dimensions that would squash the decoration.\n- **Don't assume a file exists.** Verify input paths before composing.\n- **Save every mockup** so you can diff between iterations.\n- **Lead with the reviewed result** (the PNG), then one line on the placement\n  (and any auto-adjustment you made), then ask what to tune next.\n- When ambiguous (\"put it on the chest\"), ask one clarifying question\n  (\"full front or left chest?\") rather than guessing.\n\n## When NOT to use\n\n- The user wants a brand-new logo/design *created* from a prompt → that's\n  generative image work, out of scope here.\n- The user wants vector/print-ready separations, embroidery **DST** digitizing,\n  or color-by-color screen seps → out of scope. (Preparing a **DTF** production\n  PNG at 300 DPI *is* in scope — see [Making art production-ready](#making-art-production-ready-dtf--dropping-the-art).)\n- The user wants a photoreal render with lighting/wrinkle warping → this skill\n  does flat ratio-based compositing, not 3D/displacement warping.\n\nFile v0.18.1:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-graphic-artist\",\n  \"version\": \"0.18.1\",\n  \"publishedAt\": 1790274097564\n}\n\nFile v0.18.1:references/decoration_spec.md\n\n# Decoration spec — real-world print sizes & placements\n\nGround-truth print dimensions from the **Bacon & Co. Recommended Garment &\nAccessory Decoration Guide** (`assets/decoration_guide.png` — the canonical\nplacement diagram; open it when you need to see where a location sits). Use\nthis as the reference the [self-review loop](self_review.md) judges against, so\n\"correct\" means *industry-standard*, not a guess.\n\n## Why inches map cleanly to our ratios\n\nThe spec is in inches and split adult/youth; our rules are ratios of the\ndetected garment bbox. Those are two views of the same thing: youth full-front\n(9″) ÷ adult full-front (13″) ≈ 0.69, and youth garments run ~0.69 the width of\nadult — so **one ratio reproduces both columns automatically**. That's the\nwhole reason the model is ratio-based; the spec validates it. What the spec\npins down that a lone ratio can't is the *proportion between placements* (a left\nchest is ~¼ the width of a full front) and *height caps* (a hoodie front must\nclear the pocket).\n\n## Print dimensions (adult / youth), width unless noted\n\n| # | Location | Adult | Youth | Frac of full-front width |\n|---|---|---|---|---|\n| 1 | Full front | 13″W | 9″W | 1.00 (the anchor) |\n| 2 | Center chest | 3–5″W | 3″W | ~0.31 |\n| 3/4 | Right / left chest | 3–4″W | 3–4″W | ~0.27 (pocket-sized) |\n| 5 | Pocket | 3″W × 3″H | 3″W × 3″H | ~0.23, square |\n| 6 | Full back | 13″W | 9″W | 1.00 (= full front) |\n| 7 | Upper back | 10–13″W | 7–9″W | ~0.88 |\n| 8 | Tag | 3–4″W | 3–4″W | ~0.27, high under collar |\n| 9 | Lower back | 10–13″W | 7–9″W | ~0.88, low |\n| 10/11 | Sleeve (short) | 3–4″W | 3″W | ~0.27 |\n| 12/13 | Sleeve (long) | 3.25″W × **12″H** | 3″W × 9″H | ~0.25, height-dominant |\n| 14 | Full front (hoodie) | 13″W × **9″H** | 9″W × 6″H | 1.00 width, **9″H cap (pocket)** |\n| 15/16 | Left / right chest (polo) | 3–4″W | 3″W | ~0.27 |\n| 23 | Drawstring backpack | 11–12″W | — | large, centered panel |\n| 24 | Hat front | 2.5″H max (high) / 2″H (low) | — | **height-capped**, width varies |\n| 25 | Hat back | 1.25″H max | — | small |\n| 26 | Hat side | 2″W max | — | small |\n| 27 | Visor front | 1.5″H max (high) / 1″H (low) | — | small |\n\nEmbroidery floor: lettering ≥ 0.25″H and 2 mm stroke — a mark that renders\nbelow that in the mockup is too small to actually sew.\n\n## How this shaped the shipped ratios\n\n`assets/placement_rules.json` was reconciled to the proportions above:\n\n| Placement | Was | Now | Why |\n|---|---|---|---|\n| left/right chest (all cats) | 0.18 | 0.14–0.15 | 3–4″ is ~¼ of a 13″ full front — pocket-sized, not 1/3 |\n| full_back | 0.55–0.60 | 0.50–0.55 | spec sizes full back = full front (both 13″) |\n| back_yoke / upper back | 0.40 | 0.44 | spec upper back ~10–13″ ≈ 0.88 of full front |\n| sleeve | 0.12 | 0.14 | spec sleeve 3–4″ ≈ 0.27 of full front |\n| hoodie full_front | 0.55 | 0.55 + `max_height_ratio 0.33` | 13″×9″ box — cap height so tall art clears the pocket |\n\nFull front, hat, and mug were left as-is (full front already matched; hats are\nheight-capped in ways the width-ratio model doesn't govern; no mug on the\nsheet).\n\n## Caveats when reading the sheet\n\n- **Absolute inches need a bbox width to become a ratio**, and the detected\n  bbox varies with the photo (a worn hoodie's silhouette includes sleeves; a\n  flat-lay tee doesn't). So trust the *proportions between locations* over any\n  single inch→ratio conversion.\n- **\"may vary based on artwork\"** — the sheet says so itself. These are\n  recommended maxes/typicals, not hard law. The self-review loop should treat\n  them as the target to land near, then defer to what looks right on the\n  specific garment.\n- **Height caps are estimates in ratio terms.** The hoodie 9″ pocket line ≈\n  0.33 of a ~27″ body bbox; if a specific blank is cropped differently, the cap\n  may need a nudge. It only ever scales art *down* to fit, never up.\n\nFile v0.18.1:references/iterative_feedback.md\n\n# Iterative feedback & rule tuning\n\nMockups are a conversation. The first compose is a starting point; the user\nwill react (\"bigger\", \"move it up\", \"rotate it\"). Translate plain-language\nfeedback into deltas layered on top of the *previous* run's arguments.\n\n## Feedback → flags\n\n| User says | You run (added to the last run's args) |\n|---|---|\n| \"Make it ~10% bigger\" | `--width-delta-pct +10` |\n| \"Make it ~15% smaller\" | `--width-delta-pct -15` |\n| \"Move it up a little\" | `--offset-y-pct -5` |\n| \"Move it down\" | `--offset-y-pct +5` |\n| \"Shift left\" | `--offset-x-pct -5` |\n| \"Shift right\" | `--offset-x-pct +5` |\n| \"Rotate 5° clockwise\" | `--rotate-deg 5` |\n| \"Rotate 5° counter-clockwise\" | `--rotate-deg -5` |\n\nKeep a running record of the current flags so each turn composes on top of the\nlast, not from the original defaults. A delta of `±5` percentage points is a\ngood \"a little\" nudge; `±10–15%` is a good \"noticeably\" nudge for width.\n\n## Promoting a tuned result to a default\n\nWhen the user is happy and says something like \"save that as the new default\nfor hoodie full-front,\" bake the *effective* ratios from the last compose\nreceipt's `applied` block into the catalog:\n\n```bash\npython3 scripts/edit_placement_rule.py update hoodie full_front \\\n    --width-ratio 0.58 --y-top-ratio 0.20\n```\n\n## Adding a brand-new category or placement\n\nWhen the user asks for a placement/category that isn't in the catalog yet:\n\n```bash\npython3 scripts/edit_placement_rule.py add tote front \\\n    --width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30\n```\n\nThen re-compose with that `--category`/`--placement`.\n\n## Coordinate model recap\n\nAll rule fields are ratios against the **detected garment bounding box**, not\nthe full image:\n\n- `width_ratio` — decoration width ÷ garment bbox width (0.01–1.5).\n- `x_center_ratio` — decoration center X within the bbox (0 = left, 1 = right).\n- `y_top_ratio` — decoration top Y within the bbox (0 = top, 1 = bottom).\n- `rotation_deg` — clockwise rotation in degrees (−180…180).\n\nBecause they're ratios, the same rule yields a visually matching print across\nresolutions, crops, and garment sizes (a youth tee and an adult tee line up).\n\nFile v0.18.1:references/location_dimensions.json\n\n{\n  \"_meta\": {\n    \"source\": \"Bacon & Co. Recommended Garment & Accessory Decoration Guide (references/decoration_spec_sheet.pdf)\",\n    \"units\": \"inches\",\n    \"note\": \"Max print dimensions per decoration location. Ranges are captured as the UPPER bound in max_width_in / max_height_in; the raw label is preserved. At runtime prefer the authoritative Odoo decoration_location.max_width/max_height for the specific record; use this table as a fallback / cross-check, matched by location name. 'Logo placement and sizing may vary, based on artwork provided.'\",\n    \"embroidery_minimum\": {\"lettering_height_in\": 0.25, \"stroke_mm\": 2}\n  },\n  \"locations\": [\n    {\"num\": 1,  \"location\": \"Full Front\",                \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"13\\\"W\"},        \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"9\\\"W\"}},\n    {\"num\": 2,  \"location\": \"Center Chest\",              \"adult\": {\"max_width_in\": 5.0,  \"max_height_in\": null, \"raw\": \"3\\\"-5\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 3,  \"location\": \"Right Chest\",               \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 4.0, \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"}},\n    {\"num\": 4,  \"location\": \"Left Chest\",                \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 4.0, \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"}},\n    {\"num\": 5,  \"location\": \"Pocket\",                    \"adult\": {\"max_width_in\": 3.0,  \"max_height_in\": 3.0,  \"raw\": \"3\\\"W x 3\\\"H\"},  \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": 3.0, \"raw\": \"3\\\"W x 3\\\"H\"}},\n    {\"num\": 6,  \"location\": \"Full Back\",                 \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"13\\\"W\"},        \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"9\\\"W\"}},\n    {\"num\": 7,  \"location\": \"Upper Back\",                \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"10\\\"-13\\\"W\"},   \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"7\\\"-9\\\"W\"}},\n    {\"num\": 8,  \"location\": \"Tag\",                       \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 4.0, \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"}},\n    {\"num\": 9,  \"location\": \"Lower Back\",                \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"10\\\"-13\\\"W\"},   \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"7\\\"-9\\\"W\"}},\n    {\"num\": 10, \"location\": \"Right Sleeve (short)\",      \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 11, \"location\": \"Left Sleeve (short)\",       \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 12, \"location\": \"Right Sleeve (long)\",       \"adult\": {\"max_width_in\": 3.25, \"max_height_in\": 12.0, \"raw\": \"3.25\\\"W x 12\\\"H\"}, \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": 9.0, \"raw\": \"3\\\"W x 9\\\"H\"}},\n    {\"num\": 13, \"location\": \"Left Sleeve (long)\",        \"adult\": {\"max_width_in\": 3.25, \"max_height_in\": 12.0, \"raw\": \"3.25\\\"W x 12\\\"H\"}, \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": 9.0, \"raw\": \"3\\\"W x 9\\\"H\"}},\n    {\"num\": 14, \"location\": \"Full Front (Hoodie)\",       \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": 9.0,  \"raw\": \"13\\\"W x 9\\\"H\"}, \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": 6.0, \"raw\": \"9\\\"W x 6\\\"H\"}},\n    {\"num\": 15, \"location\": \"Left Chest (Polo)\",         \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3-4\\\"W\"},       \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 16, \"location\": \"Right Chest (Polo)\",        \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3-4\\\"W\"},       \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 17, \"location\": \"Right Hip / Under Pocket\",  \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 18, \"location\": \"Left Hip / Under Pocket\",   \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 19, \"location\": \"Right Leg (side)\",          \"adult\": {\"max_width_in\": null, \"max_height_in\": 12.0, \"raw\": \"12\\\"H\"},        \"youth\": {\"max_width_in\": null, \"max_height_in\": 9.0, \"raw\": \"9\\\"H\"}},\n    {\"num\": 20, \"location\": \"Left Leg (side)\",           \"adult\": {\"max_width_in\": null, \"max_height_in\": 12.0, \"raw\": \"12\\\"H\"},        \"youth\": {\"max_width_in\": null, \"max_height_in\": 9.0, \"raw\": \"9\\\"H\"}},\n    {\"num\": 21, \"location\": \"Right Leg (front)\",         \"adult\": {\"max_width_in\": 3.0,  \"max_height_in\": null, \"raw\": \"3\\\"W\"},         \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 22, \"location\": \"Left Leg (front)\",          \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 23, \"location\": \"Drawstring Backpack\",       \"size\": {\"max_width_in\": 12.0, \"max_height_in\": null, \"raw\": \"11\\\"-12\\\"W\"}}\n  ],\n  \"headwear\": [\n    {\"num\": 24, \"location\": \"Hat Front\",   \"high_profile\": {\"max_height_in\": 2.5,  \"raw\": \"2.5\\\"H (max)\"}, \"low_profile\": {\"max_height_in\": 2.0,  \"raw\": \"2\\\"H (max)\"}},\n    {\"num\": 25, \"location\": \"Hat Back\",    \"high_profile\": {\"max_height_in\": 1.25, \"raw\": \"1.25\\\"H (max)\"}, \"low_profile\": {\"max_height_in\": 1.25, \"raw\": \"1.25\\\"H (max)\"}},\n    {\"num\": 26, \"location\": \"Hat Side\",    \"high_profile\": {\"max_width_in\": 2.0,   \"raw\": \"2\\\"W (max)\"},   \"low_profile\": {\"max_width_in\": 2.0,   \"raw\": \"2\\\"W (max)\"}},\n    {\"num\": 27, \"location\": \"Visor Front\", \"high_profile\": {\"max_height_in\": 1.5,  \"raw\": \"1.5\\\"H (max)\"}, \"low_profile\": {\"max_height_in\": 1.0,  \"raw\": \"1\\\"H (max)\"}}\n  ]\n}\n\nFile v0.18.1:references/mockup_routine.md\n\n# Batch mockup routine — fill in missing `mockup_image` on decoration requests\n\nThis is the workflow the **Mockup Artist Agent** runs on its schedule (a\nDrivethru routine). No user is watching each run; the agent picks up every\ndecoration request assigned to **Zach Tucker** that still needs a mockup,\ngenerates one for every request where both required inputs exist, and writes\nit back to Odoo's `mockup_image` field. Requests that don't have the inputs\nyet are left alone (a **skipped** verdict per request, not a failure).\n\nThe rendering itself is the same deterministic pipeline the interactive skill\nuses — this doc only wraps it in a per-request loop and the\nOdoo read/write plumbing. **Same rules still apply:** no model-generated\npixels, always self-review before writing back (see\n[`self_review.md`](self_review.md)).\n\n## What the routine does on each fire\n\n1. **Find the work.** Search Odoo for open decoration requests assigned to\n   Zach Tucker (see [Finding the queue](#1-finding-the-queue)).\n2. **For each request, decide if it's actionable.** Skip anything that\n   already has a `mockup_image`. For the rest, list the request's attachments\n   and pick a **blank product image** and a **decoration image**. If either\n   is missing, record `skipped: missing_inputs` and move on.\n3. **Generate the mockup.** Download both images to a temp file, run\n   `compose_mockup.py`, then run the mandatory self-review loop\n   ([`self_review.md`](self_review.md)) — up to 3 attempts.\n4. **Write it back.** Upload the reviewed PNG into the request's\n   `mockup_image` binary field.\n5. **Log an outcome per request** — `mockup_written`, `skipped:<reason>`, or\n   `failed:<reason>` — plus a one-line summary of the run.\n\nRequests remain in their current state; this routine only fills the mockup\nfield. Advancing state (e.g. `mark_ready`, `self_approve`) is out of scope\nand stays a human decision.\n\n## 1. Finding the queue\n\n`decoration.request` isn't queryable through `ops_search`. Reach it through\n`sale.order` instead — `sales_search_orders` accepts dotted paths across the\n`decoration_request_ids` relation, so filter the orders that carry an open\nrequest assigned to Zach:\n\n```jsonc\nsales_search_orders {\n  filters: [\n    { field: \"decoration_request_ids.user_id.name\", op: \"ilike\", value: \"Zach Tucker\" },\n    { field: \"decoration_request_ids.state\",        op: \"in\",    value: [\"created\", \"progress\"] }\n  ],\n  fields: [\"id\", \"name\", \"decoration_request_ids\"],\n  response_detail: \"standard\",\n  limit: 100\n}\n```\n\nThat returns the parent orders and each order's `decoration_request_ids`.\nFor every request id in the union, fetch the request itself:\n\n```jsonc\nsales_get_decoration_request { request_id, response_detail: \"full\" }\n```\n\nThe record you care about carries:\n\n- `user_id` — assignee (confirm it's Zach; the order-level filter can pick up\n  siblings on the same order).\n- `state` — only work `created` or `progress`; leave `ready`/`sent`/\n  `approved`/`revision`/`done`/`cancelled` alone.\n- `mockup_image` — the destination binary field. **If it's already\n  populated, skip.** (The routine is idempotent; do not overwrite existing\n  mockups.)\n- `partner_id`, `name` — for logging.\n\nCap the queue at a sane per-run number (start with 25). Better to run more\noften than to blow the whole batch on one long-tail request.\n\n## 2. Picking blank + decoration images from the request\n\nA decoration request accumulates attachments through the OWL process/\nrequirement widget plus the chatter. `decoration_list_attachments` is the\none place that enumerates all of them:\n\n```jsonc\ndecoration_list_attachments {\n  model: \"decoration.request\",\n  record_id: <request_id>,\n  only_images: true,\n  include_processes: true,\n  include_data: false      // metadata only; fetch bytes only for the pair we pick\n}\n```\n\nEach entry has a fetchable web URL and a source label. The heuristic for\nwhich is which:\n\n- **Blank product image.** Look for entries whose filename or process label\n  mentions **blank / product / garment / style** or matches the request's\n  `blank_style_number` / `blank_vendor`. It usually looks like a plain\n  product photo on white.\n- **Decoration image.** Look for entries labeled **art / logo / decoration\n  / mockup source / customer art** (or the customer-uploaded process on the\n  approval portal). It's the graphic the customer wants printed.\n\n**When it's ambiguous — skip.** If you see two candidates for either role\nand can't confidently pick, record `skipped: ambiguous_inputs` and move on;\na human will label them. **Don't guess and don't ask the user mid-run** —\nthe routine fires unattended.\n\n**When either role has no candidate — skip.** Record\n`skipped: missing_inputs` (which one) and move on. This is the *most common*\nskip; expect it.\n\nFetch the bytes for the two picked attachments by GET on their `web_url`\n(the CDN URL when offloaded) and save each to a temp file. Do **not** pull\nthem via `decoration_get_image` unless a URL is missing — the raw HTTP path\nkeeps the base64 out of the token stream.\n\n## 3. Compose + self-review\n\nSame as the interactive workflow. Pick a **placement** from the request:\n\n- If the request or its linked `decoration_id` names a location (e.g.\n  \"Left Chest\", \"Full Front\", \"Back Yoke\"), map it to the placement key\n  (`left_chest`, `full_front`, `back_yoke`, …).\n- If the request only lists the location on the process/requirement lines,\n  read it from there.\n- If nothing says, fall back to `full_front` and note it in the outcome.\n\nIf the blank photo shows a hoodie/tee/hat/mug, pass `--category` too;\notherwise omit and let the `_defaults` rules apply.\n\n```bash\npython3 scripts/compose_mockup.py \\\n    --blank /tmp/blank.jpg \\\n    --decoration /tmp/deco.png \\\n    --category <category-or-omit> \\\n    --placement <placement>\n```\n\nThen run the **[self-review loop](self_review.md)** — read the rendered PNG,\njudge it, layer corrective deltas (`--width-delta-pct`, `--offset-x-pct`,\n`--offset-y-pct`, `--rotate-deg`), re-compose. **Hard cap at 3 attempts;**\nif attempt 3 still isn't great, keep the best one and log\n`review_notes: <what's still off>` in the outcome. Never loop past 3, never\nping-pong a delta's sign.\n\nCleanup and background-removal helpers still apply if the decoration is\ndegraded / on a solid plate — same rules as\n[Background removal](../SKILL.md#background-removal-flat-art-vs-photos) and\n[Cleaning up degraded art](../SKILL.md#cleaning-up-degraded--ai-generated-art).\nUse them when the source obviously needs it; skip them by default so a clean\nPNG doesn't take the scenic route.\n\n## 4. Writing the mockup back to Odoo\n\nThe `mockup_image` field is a stored, writable binary on\n`decoration.request`. A mockup PNG is small (unlike a 300 DPI DTF file), so\nthe streaming split isn't needed. Use `decoration_set_image` directly:\n\n```jsonc\ndecoration_set_image {\n  model:       \"decoration.request\",\n  record_id:   <request_id>,\n  field:       \"mockup_image\",\n  data_base64: \"<base64 of the reviewed PNG>\"\n}\n```\n\nRead the reviewed PNG from disk, base64-encode it, and send it in one call.\nVerify the tool's response reports the field written before logging\n`mockup_written`.\n\n**Do not** advance the request's state; that's a human decision.\n\n**Do not** overwrite an existing `mockup_image`; the pre-check in step 2\nalready skipped populated requests.\n\n## 5. Per-request outcome + run summary\n\nEmit one outcome record per request the routine touched, so the run's\nverdict is legible in the Routines page:\n\n| Outcome | When |\n|---|---|\n| `mockup_written` | Compose + self-review passed and `decoration_set_image` succeeded. |\n| `skipped:already_has_mockup` | `mockup_image` was already populated. |\n| `skipped:missing_inputs` | No blank *or* no decoration attachment. |\n| `skipped:ambiguous_inputs` | Multiple candidates for a role, no confident pick. |\n| `failed:<reason>` | Compose/upload/HTTP error. Do not swallow the traceback — put it in the outcome so the human can see it. |\n\nThen log a single **run summary**: `queue=<N>, written=<w>, skipped=<s>,\nfailed=<f>`. If the queue was empty, the whole run is a `nothing_to_do`\nverdict — that's the desired steady state most of the time.\n\n## Rules specific to this routine\n\n- **Idempotent.** Never overwrite an existing `mockup_image`. A second run\n  on the same request should either write once (first run had inputs) or\n  skip forever (still missing).\n- **Unattended.** No `AskUserQuestion`. Ambiguity → skip with a reason.\n- **Bounded.** Cap the per-fire queue at 25 requests. Nothing else in this\n  container is watching wall-clock.\n- **Non-destructive.** The routine only writes the `mockup_image` binary.\n  It does not change state, add chatter attachments, edit fields, or\n  create/modify decorations.\n- **Same eyes-on rule.** Even in batch mode, the self-review loop is\n  required. A silent bad mockup is worse than a skipped one.\n\n## The routine (paste into the Mockup Artist Agent's Routines page)\n\n- **Name:** `Zach Tucker — fill missing mockups`\n- **Schedule:** every hour on the :07 mark\n  (cron `7 * * * *`, local time)\n- **Per-day cap:** 24 (one per hour)\n- **Prompt:**\n\n  ```\n  Fill in missing mockups for decoration requests assigned to Zach Tucker.\n  Follow references/mockup_routine.md in the drivethru-graphic-artist skill:\n  search sale.order for orders whose decoration_request_ids.user_id.name is\n  \"Zach Tucker\" and whose state is in [\"created\",\"progress\"]; for each\n  request, skip if mockup_image is already set; otherwise pick a blank\n  product image and a decoration image from decoration_list_attachments; if\n  both exist, generate a mockup with compose_mockup.py, self-review up to 3\n  attempts, and write the reviewed PNG back to mockup_image via\n  decoration_set_image. Cap the queue at 25. Log one outcome per request\n  (mockup_written / skipped:<reason> / failed:<reason>) plus a run summary.\n  Never overwrite an existing mockup_image, never advance state, never ask\n  a human — ambiguity is a skip.\n  ```\n\nFile v0.18.1:references/placement_rules_schema.json\n\n{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"Placement Rules Catalog\",\n  \"description\": \"Category x placement -> ratio-based placement rules. Top-level keys are garment categories (hoodie, tee, hat, mug, ...) plus the special '_defaults' key used as a fallback when a category-specific rule isn't defined.\",\n  \"type\": \"object\",\n  \"patternProperties\": {\n    \"^[a-z][a-z0-9_]*$\": {\n      \"type\": \"object\",\n      \"patternProperties\": {\n        \"^[a-z][a-z0-9_]*$\": {\n          \"type\": \"object\",\n          \"required\": [\"width_ratio\", \"x_center_ratio\", \"y_top_ratio\"],\n          \"properties\": {\n            \"width_ratio\": {\n              \"type\": \"number\",\n              \"minimum\": 0.01,\n              \"maximum\": 1.5,\n              \"description\": \"Decoration width as a fraction of the detected garment bbox width.\"\n            },\n            \"x_center_ratio\": {\n              \"type\": \"number\",\n              \"minimum\": 0.0,\n              \"maximum\": 1.0,\n              \"description\": \"Decoration center X position as a fraction of the garment bbox width, in IMAGE coordinates (0 = image left edge, 1 = image right edge). Placement names use the WEARER's left/right: on a flat FRONT photo the wearer's left chest/sleeve is on the IMAGE RIGHT (x > 0.5) and the wearer's right is on the image left (x < 0.5).\"\n            },\n            \"y_top_ratio\": {\n              \"type\": \"number\",\n              \"minimum\": 0.0,\n              \"maximum\": 1.0,\n              \"description\": \"Decoration top Y position as a fraction of the garment bbox height (0 = top edge, 1 = bottom edge).\"\n            },\n            \"rotation_deg\": {\n              \"type\": \"number\",\n              \"minimum\": -180,\n              \"maximum\": 180,\n              \"default\": 0,\n              \"description\": \"Default rotation applied to the decoration, in degrees. Positive is clockwise.\"\n            },\n            \"max_height_ratio\": {\n              \"type\": \"number\",\n              \"minimum\": 0.01,\n              \"maximum\": 1.5,\n              \"description\": \"Optional cap on decoration height as a fraction of the detected garment bbox height. If the aspect-locked height from width_ratio exceeds this, the decoration is scaled down (aspect preserved) so it fits the location's height box (e.g. a hoodie full-front print must fit above the pocket ~= 9in). Omit for width-only sizing.\"\n            }\n          },\n          \"additionalProperties\": true\n        }\n      },\n      \"additionalProperties\": false\n    }\n  },\n  \"additionalProperties\": false\n}\n\nFile v0.18.1:references/production_cleanup.md\n\n# Cleaning up degraded / AI-generated art before production\n\nSome art that comes in for a \"drop\" isn't clean vector — it's **\"fake vector\":**\na logo an image model generated (or someone up-scaled from a tiny JPEG) that\n*looks* crisp in a 200px thumbnail but is actually a soft raster full of the\ndefects below. Nobody notices at thumbnail size. Then it prints at **13″** and\nthe flaws are obvious and un-sellable.\n\nThis is the step where you catch that — the same \"you have eyes, the pipeline\ndoesn't\" discipline as the mockup [self-review](self_review.md), applied to the\n*art itself* before you size and drop it. **Look at the art at production scale,\ndecide if it's degraded, and if so clean it deterministically first.**\n\n## Recognize the defects (this is the judgment call)\n\nZoom in (or render the art large and `Read` it). You're looking for the\nsignature of generated / mangled flat art:\n\n| Defect | What it looks like | Why it matters at 13″ |\n|---|---|---|\n| **Ghosting** | a faint offset *second copy* of the marks; doubled edges | prints as a blurry shadow next to every edge |\n| **Haze / bleed** | a desaturated fog around the inks, on transparency or over a fill | muddy halo, dirty edges |\n| **Faded / broken outline** | a stroke that thins, breaks up, or disappears along its length (classic: one side of a word is crisp, the other side \"fades out\") | outline looks like it's dissolving |\n| **Jagged / uneven width** | an outline that should be one weight wobbles thick↔thin and waves | reads as sloppy, not a real logo |\n| **Soft / blurry edges** | no crisp ink boundary; everything slightly out of focus | fuzzy print, no snap |\n\nThe tell that ties them together: **the art is clearly *meant* to be a few flat\nspot-color inks with crisp uniform edges, but the pixels don't deliver that.**\nThat gap between intent and pixels is what you're fixing. If the art is already\nclean flat vector-style color, or it's a photograph, this doesn't apply — skip it.\n\n## The principle: clean up ≠ regenerate\n\nA human artist handed this file for a 13″ print does **not** try to salvage the\nblurry pixels, and does **not** feed it to an image generator to \"redo it\" — a\ngenerative model would rewrite the letterforms, mangle the text, and invent\ndetails, i.e. produce a *different* logo. They rebuild it as what it was always\nmeant to be: **the handful of inks it already contains, snapped clean, broken\nstrokes rejoined, edges re-rendered crisp.**\n\nThat is exactly what `scripts/cleanup_art.py` does, and it is **deterministic —\nno model-generated pixels** (same rule as the rest of this skill). It only\nre-expresses inks that are already present; it never invents art. Reach for a\ngenerative model **only** if the user explicitly asks for a redesign and accepts\nthat the letterforms/text will change — and say so first.\n\n## Procedure\n\n### 1. Get the true inks — prefer the decoration's declared colors\nThe cleanup keys on the real ink colors. The **best** source is the decoration's\nown color list (`colors[].rgb_hex` from `decoration_get_production_readiness`) —\nthat's ground truth, operator-entered. Pass them with `--inks`:\n\n```bash\npython3 scripts/cleanup_art.py --input /tmp/thumb.png \\\n    --inks '#26296B,#A0202C,#FFFFFF'      # decoration colors[].rgb_hex\n```\n\nIf there's no declared list, let it auto-detect (`--colors N`), but **auto-detect\nis a fallback and can lump a small ink (a thin red outline) into a big fill** —\nverify the reported `inks` look right, and prefer declared colors whenever you\nhave them.\n\n### 2. Run the cleanup\n```bash\npython3 scripts/cleanup_art.py --input /tmp/thumb.png \\\n    --inks '#26296B,#A0202C,#FFFFFF' \\\n    --method vector \\                      # default; crispest edges for print\n    --output /tmp/thumb_clean.png\n```\nIt classifies every pixel to its nearest ink (dropping the haze/ghost),\ndespeckles + closes each ink mask to rejoin broken outlines, and re-renders each\nmask crisp at print resolution — the **vector** method traces the mask and\nsmooths it with a **corner-preserving** filter (pins sharp letter corners, smooths\nonly the straight runs), so you get clean edges *without* rounding off the\nletterforms. It writes the cleaned PNG plus a **`*_proof.png`** and a JSON receipt\n(ink palette, per-ink pixel counts, params, print size, and a `review_checklist`).\n\n### 3. Self-review the proof (required — do not skip)\n`Read` the `*_proof.png`. It shows before vs after on gray and on a dark garment,\nplus a **zoom on the outline detail where the defects hide.** Judge it against the\n`review_checklist`:\n\n- **Outlines** crisp, continuous, uniform width? (the fade/break/jaggies gone?)\n- **Corners** of letters/marks still sharp — not rounded by the smoothing?\n- **Letterforms / text unchanged** — nothing merged, dropped, or invented?\n- **Ghost/haze gone**, no stray specks, colors match the true inks?\n- Anything that was **missing** (not just faded) is still missing — this tool\n  can't add a mark that isn't there. Flag it if so.\n\nGood → continue the drop with the cleaned PNG. Off → tune and re-run (below).\nSame discipline, same spirit as the mockup loop: **the customer sees the reviewed\nresult, not the first pass.**\n\n### 4. Tune when the review finds a problem\nLayer one change at a time; re-run; re-read the proof.\n\n| What you see in the proof | Change |\n|---|---|\n| Sharp corners look rounded / letters softened | `--corner-deg 30` (protect more corners) and/or `--smooth-iters 8` (smooth less) |\n| Outline still broken / not rejoined | `--close 2` (or `3`) to bridge bigger gaps |\n| Haze/ghost still present | `--tolerance 45` (snap tighter) and/or `--alpha-min 140` (drop faint pixels) |\n| A real thin/faint mark got dropped | `--tolerance 90` (snap looser) and/or `--alpha-min 70` |\n| Tiny real details erased as \"specks\" | `--min-speck 6` |\n| Edges too soft (raster method) | switch to `--method vector`, or lower the raster ramp |\n| Still fighting curve smoothness at huge sizes | raise `--supersample 6`, or set `--long-edge-px` to your target |\n\nDon't ping-pong a value's direction; if you overshoot, you're close — accept the\nbetter result. If two or three tuned runs still don't nail it, hand back the best\nwith a one-line note on what's off (e.g. \"the R in the emblem is genuinely missing\na leg in the source — needs a redraw, not a cleanup\").\n\n### 5. Feed the cleaned PNG into the normal drop\nUse `/tmp/thumb_clean.png` as the input to `prepare_dtf_production.py` (size to\nthe location box, 300 DPI) and `extract_colors.py`, then upload — the standard\n[production-ready](production_ready.md) flow from there. Cleanup slots in **after**\nyou pull the thumbnail / knock out any plate, and **before** you size it.\n\n## Faithful vs. \"improved\" — stay on the cleanup side of the line\n\nCleanup restores *intent*; it does not redesign. Keep these straight:\n\n- **Preserve intended style.** If letters are drawn **hollow** (outline + inline,\n  garment shows through the center), that's a design choice — keep it. Don't\n  \"helpfully\" flood the centers solid. (The West Football wordmark is exactly\n  this: `WEST` is solid-filled, `FOOTBALL` is hollow. Both are correct.)\n- **Preserve counters.** The holes in O/A/B/R/D stay open — the tracer treats them\n  as holes automatically; don't fill them.\n- **Snap colors, don't restyle them.** Cleanup maps pixels to the inks that are\n  already there. Changing the palette, adding a color, or recoloring is a design\n  change — ask.\n- **Can't invent.** Faded/broken = restorable (the ink is thinly there). *Missing*\n  = not restorable here; flag it for a human redraw.\n- **When intent is genuinely ambiguous**, ask — show the rendered options, the\n  same way the knockout flow does for `flood` vs `color-to-alpha`.\n\n## When to use something else\n\n- **Photographic** subject → this is for flat art; use `remove_background.py`\n  (rembg) to isolate a photo subject.\n- **Solid plate to knock out** (logo on a white/colored background) → do the\n  [knockout](../SKILL.md#background-removal-flat-art-vs-photos) first\n  (`knockout_color.py`), then clean up if still degraded.\n- **Redesign / new artwork / restyle** → out of scope for cleanup; that's\n  generative/manual design work — confirm with the user before going there.\n- **Already-clean art** → don't \"clean\" crisp vector-style art for no reason;\n  you'll only risk softening good corners. Only run this when the art is actually\n  degraded.\n\n## Worked example — West Football (AI-generated)\n\nSource: a 1024² PNG that reads fine as a thumbnail. At size: `WEST` had a ghosted\ndouble outline; `FOOTBALL`'s navy outline **faded out and broke up** on the left\nhalf and wobbled in width; desaturated haze around everything; soft edges.\n\n```bash\npython3 scripts/cleanup_art.py --input west.png \\\n    --inks '#26296B,#A0202C,#FFFFFF' --method vector --output west_clean.png\n```\n\nReceipt: 3 inks (navy/red/white), output ≈ 3953×2702 px = **13.18×9.01″ @ 300 DPI**.\nProof review: ghosting/haze gone; the `FOOTBALL` navy outline is now solid and\nuniform across the whole word; edges crisp; W/E/S/T corners still sharp; hollow\n`FOOTBALL` centers preserved. One judgment call confirmed by sampling the source —\nthe hollow letters are *intended*, not a defect — so they were kept, not filled.\nResult was production-ready to size and drop.\n\nFile v0.18.1:references/production_ready.md\n\n# Making art production-ready — \"dropping\" a DTF decoration\n\n**Production-ready** (internally: *dropping the art*) means driving a\n`decoration` record to the **`done`** state. This almost always applies to\ndecorations whose `decoration_method` is **DTF**. It is deterministic\nimage work — take the decoration's **real** thumbnail, size it to the location,\nrender it at 300 DPI, and record the facts back into Odoo. **No pixels are\nmodel-generated** (same principle as the mockup pipeline).\n\nYou drive this through the `drivethru_mcp` **`decoration_*`** MCP tools plus the\ntwo skill scripts below. Give the agent a decoration id and it does the rest.\n\n> **Transport — two rules.**\n> 1. Every **non-image** step below (readiness, sizes, colors, state, sample) is\n>    an **MCP tool call** — invoke it directly, like any other tool. There is\n>    **no `/call` route**; a 404 from one is a hand-rolled request that shouldn't\n>    exist, so call the tool instead, and don't go hunting for an\n>    `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` to POST against for it.\n> 2. Moving image **bytes** is the one place raw HTTP is right — only to keep\n>    megabytes of base64 out of your token stream: **download** a `cdn_url` that\n>    `decoration_get_image` returns for a large/offloaded image (step 2), and\n>    **upload** the production file with `upload_production_file.py` when\n>    `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are set (step 5). Both byte paths still hit\n>    the same 20 MB server guard as the tools.\n\n## What `done` requires (DTF)\n\n`decoration_get_production_readiness` returns a `blocking_gaps` list that mirrors\nOdoo's own gate. To reach `done` a DTF decoration needs:\n\n- `dtf_production_png` uploaded (→ `decoration_production_ready`)\n- non-zero `size_width` **and** `size_height` (inches)\n- `colors` populated **and** `len(colors) == color_count`\n- a printed sample, if `artwork_module_custom.dtf_sample_required` is on\n- a `design_id` whose own `state == 'done'`\n- a real `name` (must not contain the word \"decoration\")\n\n## The procedure\n\n### 1. Read the current state\n`decoration_get_production_readiness { decoration_id }`. Confirm `is_dtf` is\ntrue, note the `location.max_width` / `max_height` (inches) and the current\n`blocking_gaps`. If the location has no max set, fall back to\n[`location_dimensions.json`](location_dimensions.json), matching by location\nname (adult column unless the order is youth).\n\n### 2. Pull the thumbnail\n`decoration_get_image { model:\"decoration\", record_id, field:\"image\" }`. A small\nimage returns inline `data_base64`; a large or CDN-offloaded one returns a\n`cdn_url` instead (with a note saying so) — that's **expected, not an error**.\nWhen you get a `cdn_url`, fetch it directly with the container's HTTP client\n(`curl` / `requests`) so the bytes skip your token stream. Save the result to a\ntemp file.\n\n**If the thumbnail has a solid background (e.g. a logo on a plate) and needs a\nclean cutout, knock the color out with `scripts/knockout_color.py` — not rembg.**\nA DTF thumbnail is flat art, so color-keying gives crisp edges with no halo;\nrembg (`remove_background.py`) is for photographic subjects and leaves a soft\nfringe on a logo. Do **not** read print intent from the thumbnail's background —\na white plate does not tell you whether white prints. The decoration's **color\nlist** tells you that, so drive the cutout from it.\n\n**Analyze first, with the declared inks.** Pass `colors[].rgb_hex` from the\nreadiness call as `--expect-colors`:\n\n```bash\npython3 scripts/knockout_color.py --input /tmp/thumb.png --analyze \\\n    --expect-colors '#000000,#FFFFFF'   # <- the decoration's colors[].rgb_hex\n```\n\nRead the receipt:\n- `key_is_declared_ink` **true** → the background color prints → keep it →\n  `--mode flood` (die-cut; enclosed areas of that color stay).\n- `key_is_declared_ink` **false** → it's background → knock it out →\n  `--mode color-to-alpha` (default; the garment shows through open/interior areas).\n- Let `enclosed_fraction` / `decision_matters` calibrate your care: near-zero means\n  the modes are equivalent here; large means it's a consequential call.\n\nThen cut with the chosen mode:\n\n```bash\npython3 scripts/knockout_color.py --input /tmp/thumb.png --output /tmp/thumb_cut.png \\\n    --mode color-to-alpha --expect-colors '#000000,#FFFFFF'\n```\n\n**The color list is a guide, not gospel** — it's operator-entered and can be\nstale. So when `decision_matters` is true, **`Read` the cutout before moving on**\n(same self-review discipline): confirm the plate is gone, edges are clean, and any\ninterior color you meant to keep is still there. Ask the user only when the inks\ndon't resolve it or contradict what you see — showing both rendered options. Then\nfeed the cutout into step 3. Running `extract_colors.py` on a proper cutout also\nyields a clean color list (transparent plate ignored) instead of a stray white.\n\n> **Thumbnails vs production files.** The cutout is the *production* file\n> (`dtf_production_png`); it belongs on transparency. Don't overwrite the `image`\n> thumbnail with it — a bare cutout makes a poor thumbnail (white art vanishes in\n> the DB). If a thumbnail needs to be legible, render one with\n> `scripts/thumbnail_card.py` (art over a neutral gray/checker card). Thumbnails\n> today carry whatever background was supplied — don't assume they're normalized.\n\n### 2.5 Clean up degraded / AI-generated art (if needed)\nBefore you size it, **look at the art at production scale.** A DTF file prints at\nup to 13″ — defects invisible in the DB thumbnail are glaring there. If the art is\ndegraded / AI-generated — **ghosting, desaturated haze, faded or broken outlines,\njagged uneven strokes, soft edges** — clean it first, or you'll drop a bad file.\n\nRestore, don't regenerate: snap the art back to its true inks, rejoin the broken\noutlines, and re-render crisp — deterministically, **no model-generated pixels**\n(a generative model would rewrite the letterforms). Pass the decoration's declared\ncolors as the ink truth:\n\n```bash\npython3 scripts/cleanup_art.py --input /tmp/thumb_cut.png \\\n    --inks '#000000,#FFFFFF' \\            # colors[].rgb_hex from the readiness call\n    --output /tmp/thumb_clean.png\n```\n\nThen **`Read` the `*_proof.png`** it writes (before/after + an outline zoom) and\nself-review it against the receipt's `review_checklist` — outlines crisp and\nuniform, corners still sharp, letterforms unchanged, ghost/haze gone. Tune and\nre-run if off (see the tuning table in\n[`production_cleanup.md`](production_cleanup.md)). If the art is already clean flat\nvector, skip this. If a mark is genuinely *missing* (not just faded), cleanup can't\ninvent it — flag it for a redraw. Feed the cleaned PNG into step 3.\n\n**Full guide, defect-recognition, and tuning: [`production_cleanup.md`](production_cleanup.md).**\n\n### 3. Size + 300 DPI (deterministic)\n```bash\npython3 scripts/prepare_dtf_production.py \\\n    --input /tmp/thumb.png \\\n    --max-width-in <location max_width> --max-height-in <location max_height> \\\n    --dpi 300 \\\n    --output /tmp/dtf_production.png\n```\nIt fits the art (aspect-locked) into the location box, stamps 300 DPI, and prints\na receipt with the **actual** `print_inches` (width/height) and `output_px`. Keep\nthose inches — they become the decoration's size. Pass explicit\n`--target-width-in/--target-height-in` only when the order specifies a size.\n\n### 4. Extract the colors\n```bash\npython3 scripts/extract_colors.py --input /tmp/dtf_production.png --max-colors 6\n```\nReturns dominant colors as `{rgb_hex, coverage_pct}`. Tune `--max-colors` /\n`--min-coverage`, and add `--ignore-white` when the art sits on a white plate you\ndon't want counted. Sanity-check the list by eye (open the PNG) — drop stray\nanti-aliasing colors.\n\n### 5. Upload the production file\n\nA real 300 DPI DTF file is large, and emitting it as base64 through your token\nstream is slow and error-prone — that's *why* the HTTP upload path exists. Both\npaths below enforce the same 20 MB server guard; pick by whether the upload\ncredentials are present.\n\n**Preferred when `ODOO_MCP_URL` + `ODOO_MCP_TOKEN` are set — the HTTP upload\nhelper.** POST the file straight from disk; the bytes never touch your token\nstream:\n```bash\npython3 scripts/upload_production_file.py \\\n    --file /tmp/dtf_production.png --record-id <id> --field dtf_production_png\n# defaults to ODOO_MCP_URL / ODOO_MCP_TOKEN (or pass --base-url / --api-key).\n# Prints the server JSON: present + cdn_url/web_url.\n```\nThis POSTs to `/drivethru_mcp/v1/upload`; the server base64-encodes and writes\nthe field (CDN offload → production-ready), byte-exact.\n\n**Fallback when those env vars aren't set — the `decoration_set_image` MCP\ntool.** Needs no extra environment, and writing `dtf_production_png` uploads to\nthe CDN on save and flips the decoration production-ready:\n`decoration_set_image { model:\"decoration\", record_id, field:\"dtf_production_png\",\ndata_base64:<the prepared PNG> }`. A 300 DPI file is almost always too big for\none call, so stream it: split the raw PNG into N slices, base64-encode each on\nits own, and send one call per slice sharing an `upload_id` with `chunk_index`\n(0-based) + `chunk_count:N`; the field is written when the last slice lands\n(`complete:true`).\n\nEither way: if the env vars are missing, use the tool; if they're present, use\nthe helper — don't invent a `/call` or other route to bridge the two.\n\n### 6. Record the actual size\n`decoration_update_fields { decoration_id, fields:{ size_width:<in>, size_height:<in> } }`\nusing the `print_inches` from step 3.\n\n### 7. Assign the colors\n`decoration_match_colors { decoration_id, colors:[{rgb_hex}, ...] }` with the hex\nlist from step 4. The tool maps each hex to the nearest existing `color` record\n(CIEDE2000 over `rgb_hex`), or creates one from the nearest approximate-PANTONE\nentry, then sets `colors` + `color_count`. PMS matching lives server-side in Odoo\n(that's where the color library and the approximate table are). Note: exact spot\nPMS is proprietary — DTF is a full-process print, so treat matched PMS names as\nclose references, not guarantees.\n\n### 8. Create the print sample\n`decoration_create_sample { decoration_id }` — stands up a `decal.demand` sample\nalready in the **`ready`** state, so it can be printed. Idempotent.\n\n### 9. Try to mark it done\n`decoration_set_state { decoration_id, state:\"done\" }`. Today this will almost\nalways come back `blocked=true` with **\"link a completed design\"** — that gate is\nexpected until it's loosened. Report the remaining gap to the user; everything\nelse (production file, size, colors, ready sample) is already in place.\n\n## Tips\n\n- Always **look at** the prepared PNG and the extracted colors before uploading\n  (same self-review discipline as mockups) — a 300 DPI file at print size is what\n  the shop will actually run.\n- Re-run `decoration_get_production_readiness` at the end to confirm only the\n  design gap (or a sample, if required) remains.\n- Only DTF is fully supported here. Embroidery has different production files\n  (DST/PNG) and is exempt from the color-count gate.\n\nFile v0.18.1:references/self_review.md\n\n# Self-review loop — critique your own mockup before returning it\n\nThe compose pipeline is deterministic: it places the decoration by ratio\nrules against the detected garment bbox. Those rules are a *starting guess*.\nFor a specific garment/decoration pair the guess can land wrong — too high,\ntoo small, off-center — and nothing in the deterministic path can notice,\nbecause it has no eyes.\n\n**You do.** You are the model running this skill, and your `Read` tool\nrenders a PNG visually. So after every compose, look at the mockup you just\nproduced, judge it against what the placement is *supposed* to look like, and\nif it's off, re-compose with corrective deltas — up to **3** total compose\nattempts — before you return anything to the user. The user should see the\ngood result, not the first draft.\n\nThis is not generative work: no pixels are model-made. The compose stays\n100% deterministic; you are only *judging* the output and adjusting the same\nnumeric flags a human would (\"move it down, a bit bigger\").\n\n## The loop\n\n1. **Compose** with the current flags. Note the `output` path from the receipt.\n2. **Read the output PNG** (the `Read` tool renders it — actually look at it).\n3. **Score it** against the placement intent below. Decide: acceptable, or not?\n4. If **acceptable** → return the PNG to the user. Done.\n5. If **not acceptable** and you have attempts left → derive deltas (below),\n   re-compose *layering them on the previous run's flags*, go to step 2.\n6. **Hard cap: 3 compose attempts.** If attempt 3 still isn't great, return\n   the best of the three and tell the user in one line what's still off and\n   offer to keep tuning. Never loop past 3 silently.\n\n## What \"correct\" means per placement\n\nJudge the rendered image against these. They're distilled from the real print\nspec in [`decoration_spec.md`](decoration_spec.md) (with the canonical diagram\nat `assets/decoration_guide.png`) — open those when you need the exact inches or\nto see where a location sits. The size column is anchored to a full front (the\nbiggest chest print), so the key relationships are: **a chest logo is\npocket-sized, ~¼ the width of a full front**, and **a full back equals a full\nfront**.\n\n| Placement | Horizontally | Vertically | Size (spec) |\n|---|---|---|---|\n| `full_front` | centered | top of print ~1/4 down the chest, below the collar/seams | 13″ adult ≈ fills the chest, ~50–55% of garment width |\n| `left_chest` / `right_chest` | over the pec, ~1/4 in from center — **wearer's** side: `left_chest` is on the IMAGE's RIGHT half of a front photo, `right_chest` on the image's left | high on the pec, below the collar | 3–4″, pocket-sized (~¼ of a full front) |\n| `full_back` | centered | upper-to-mid back, below the yoke seam | 13″, same size as a full front |\n| `back_yoke` / upper back | centered | high, just below the collar seam | 10–13″, nearly full-back width |\n| `left_sleeve` / `right_sleeve` | centered on the sleeve — wearer's side again (`left_sleeve` = image right) | mid-forearm area | 3–4″, narrow, follows the sleeve |\n| `front` (generic) | centered | centered in the available panel | ~45–55% |\n| hoodie `full_front` | centered | on the chest, **above the pocket** | fills width but capped ~9″ tall to clear the pocket |\n\nSanity floor from the spec: embroidery lettering must be ≥ 0.25″ high — if text\nin the mockup is so small it would be illegible/unsewable, it's too small\nregardless of the table.\n\nAlso check, regardless of placement:\n\n- **Not clipped or bleeding** off the garment edge, collar, or seams.\n- **Not crooked** unless rotation was intended.\n- **Legible** — text/marks aren't so small they disappear or so large they\n  distort.\n- **Right panel** — a front print isn't riding up onto the collar or shoulder.\n\n## From critique to deltas\n\nTranslate what's wrong into flags, layered on the previous run's args. These\nare the *same* flags the iterative-feedback table uses — you're just the one\ndeciding them now instead of the user.\n\n| What you see | Delta to add |\n|---|---|\n| Sitting too high (up on the collar/seams) | `--offset-y-pct +6` to `+10` |\n| Sitting too low | `--offset-y-pct -6` to `-10` |\n| On the wrong side (e.g. `left_chest` on the image's left) | mirror it: `--offset-x-pct` ≈ `(1 − 2·x) × 100` — or better, fix the rule |\n| Too far left | `--offset-x-pct +4` to `+8` |\n| Too far right | `--offset-x-pct -4` to `-8` |\n| Too small / weak | `--width-delta-pct +10` to `+15` |\n| Too big / distorting / bleeding off edges | `--width-delta-pct -10` to `-15` |\n| Crooked (and shouldn't be) | `--rotate-deg 0` |\n\nUse a **small** correction when it's slightly off and a **larger** one when\nit's clearly off. Don't stack more than two or three deltas in one attempt —\nfix the biggest problem first.\n\n## Stop conditions (don't loop forever)\n\n- **Accept early.** \"Good enough for a customer to approve\" is the bar, not\n  pixel perfection. If attempt 1 already looks right, return it — don't burn\n  attempts chasing marginal gains.\n- **Hard cap at 3 composes.** Always.\n- **Oscillation guard.** If a delta's sign flips between attempts (you moved\n  it down, now you want it up), you overshot — you're already close. Halve the\n  correction, or just accept the better of the two. Do not ping-pong.\n- **No-progress guard.** If an attempt didn't visibly improve things, don't\n  repeat the same delta harder — stop and hand back to the user with a note.\n\n## When the same correction keeps recurring — fix the rule, not the mockup\n\nThe loop patches each mockup individually, but if you find yourself applying\nthe *same* delta in the *same direction* for a given `(category, placement)`\nacross different jobs, that's not a per-mockup quirk — the **default rule is\nsystematically off** and every future mockup will start wrong and cost a\ncorrection pass.\n\nExample: hoodie `full_front` ships with `y_top_ratio 0.22`, but the detected\nbbox includes the hood, which pulls the top reference up, so the print\nconsistently lands high and review keeps adding `--offset-y-pct +8`\n(≈ `y_top 0.30`). When you notice that pattern, offer to bake the correction\ninto the catalog so the loop rarely has to fire:\n\n```bash\npython3 scripts/edit_placement_rule.py update hoodie full_front --y-top-ratio 0.30\n```\n\nPromote from the *effective* ratios in the last good compose receipt's\n`applied` block (see `references/iterative_feedback.md` → \"Promoting a tuned\nresult to a default\"). Ask before writing — a default change affects every\nfuture mockup for that category/placement, so confirm the user wants it.\n\n## What to tell the user\n\nLead with the final PNG. Then one line: what placement, and — only if you\niterated — that you auto-adjusted it (e.g. \"nudged it down onto the chest and\nsized it up ~10% after review\"). If you hit the 3-attempt cap without nailing\nit, say what's still off and offer to keep tuning. Keep the internal critique\nto yourself; the user wants the result and a short summary, not a play-by-play.\n\nFile v0.18.1:references/web_image_routine.md\n\n# Web-image routine — per-color storefront mockups (blank + decorations)\n\nThis is what the **Mockup Artist Agent** runs for the routine **\"General Purpose\nWeb image generation routine.\"** When a storefront site is built, products are\ncreated in bulk from blanks + decorations and arrive with **no image**. This\nroutine gives every colour of every such product a mockup: the product's\ndecoration(s) composited onto that colour's blank photo, written to the colour's\n`product.product` records (every size) and, if missing, the template.\n\n**The whole routine is one script: `scripts/web_mockup_job.py`.** It plans,\nfetches, preps art, composites, checks, gets an independent vision verdict, and\nuploads — and it prints only JSON. Your job is to run it, read its JSON, act on\n`needs_adjustment` verdicts with one command, and report.\n\n## Hard rules — read these first\n\nThese exist because each one was broken in a past run.\n\n1. **Never let image bytes into your context.** Do not `Read`/open/view any\n   image file. Do not call `decoration_get_image`, `decoration_list_attachments`\n   with `include_data`, or `product_read`/`ops_search` with `include_binary`.\n   Do not `cat`/`base64` an image. The vision review runs *inside* the script\n   and hands you a yes/no verdict. (A few images in context balloons the session.)\n2. **Never write an image yourself.** No `product_write`/`product_create` with an\n   image field (the server refuses it anyway), no `decoration_set_image`, **no\n   chunking bytes into a field** — it never works. The script uploads\n   byte-exact through `POST <ODOO_MCP_URL>/upload` and verifies the write.\n3. **Never decide whether a blank \"has an image\" by fetching it.** Odoo serves a\n   *placeholder* for an empty image field. `web_mockup_plan` checks the actual\n   field server-side; an empty blank is a `skip_reason`, not a picture.\n4. **Left/right are the WEARER'S.** \"Left chest\" is over the wearer's left pec,\n   which in a flat front photo is on the **image's RIGHT** half. You never have\n   to work this out: `scripts/locations.py` maps each raw location name to a\n   `placement` (wearer frame) *and* `image_side` (image frame) — \"Left Leg\" is\n   the front of the wearer's left leg, image right — the rules are\n   orientation-guarded, the script\n   checks the side geometrically, and the reviewer is told both frames. If you\n   ever pass a manual `--offset-x-pct`, **positive moves toward the image's\n   right**.\n5. **Never composite a background.** Art is taken best-first: embroidery PNG /\n   DTF production PNG before the decoration thumbnail. Every source is checked\n   for a plate — including production PNGs that have transparent padding around\n   a solid swatch (an embroidery preview on fabric colour): a one-colour plate is\n   keyed out (`art_sources`… `plate_keyed`), and if the art's edge is still a\n   solid block that source is rejected and the next is tried. Don't work around a `background_not_removed` / `art_unusable`\n   failure; report it.\n6. **Only a \"yes\" is written.** Up to 3 attempts per colour. After that it is\n   `rejected` and left for a human. Never force it.\n7. **Unattended.** No questions to a human mid-run. Skip with a reason instead.\n\n## Setup (once per environment)\n\nThe script reads `ODOO_MCP_URL` (`…/drivethru_mcp/v1`) and `ODOO_MCP_TOKEN` —\nthe same Odoo credentials the MCP uses, **pointing at the environment you are\nwriting to** — plus a vision key: `MOCKUP_VISION_API_KEY` (or\n`OPENROUTER_API_KEY`), optional `MOCKUP_VISION_MODEL` (default\n`anthropic/claude-sonnet-5`) and `MOCKUP_VISION_BASE_URL` (default OpenRouter).\nIf a required var is missing, the script exits with a clear error — report it;\ndo not try another path.\n\n## Each fire\n\n### 1. Pick scope\n\n```jsonc\nstorefront_list_sites { \"state\": \"draft\" }     // stores being built\n```\n\nWork one site at a time, newest first. (Optionally peek with\n`web_mockup_plan { \"site_id\": <id>, \"limit\": 10 }` — it is metadata only and\nshows `colors_needing_image`. `0` → nothing to do for that site.)\n\n### 2. Run — in the background, and report progress while it goes\n\nA run takes about a minute per colour, so a store can take 15–30 minutes. If\nyou run it as one blocking command, you (and whoever is watching the task) see\nnothing until it ends. So **start it in the background and poll it**:\n\n```bash\nnohup python3 scripts/web_mockup_job.py run --site-id <id> --limit 10 \\\n    > /dev/null 2>&1 &\n# or specific products:  … run --product-tmpl-ids 24921,24920 …\n```\n\nThen, **about once a minute**, until the run is finished:\n\n```bash\npython3 scripts/web_mockup_job.py status --brief\n```\n\n```json\n{\"active_run\": {\"pid\": 4121, \"done\": 5, \"total\": 16, \"alive\": true,\n                \"latest\": \"21:24:10 [5/16]  24941-black  needs_adjustment  too_high(+6y)  a1  $0.013\"},\n \"last_run_finished\": \"2026-09-22 21:10:02\",\n \"recent\": [\"…\", \"…\", \"…\"]}\n```\n\n- **Each poll, post a progress update quoting `active_run.latest`** and\n  `done/total` (e.g. `5/16 — 24941-black needs_adjustment too_high(+6y)`), with\n  progress ≈ `done/total`. Don't repeat identical updates; post when it changes.\n- **Finished** = `active_run` is `null` and `last_run_finished` is newer than when\n  you started. Then read the result: `python3 scripts/web_mockup_job.py status --last-run`\n  (the same JSON a foreground run prints).\n- **`alive: false`** while `active_run` is still present = the process died\n  without finishing. Report the `recent` lines; don't restart it more than once.\n- Don't poll faster than every ~45 s, and don't open or read the progress log or\n  any image yourself — `status` is the only thing you need.\n\n`adjust` commands (step 3) take a minute or two each; run those in the\nforeground and post a progress update after each.\n\n**Missing blank photos are filled first, automatically.** Before planning, `run`\ncalls `web_mockup_refresh_blanks`: every blank colour with no photo (or only an\non-model one) whose blank comes from an **integrated vendor** gets its\n`vendor_image_url` set from that vendor — SanMar's SDL flat front, S&S's\n`colorFrontImage` — so it gets a mockup in the same run. Only that URL is\nwritten, never downgraded. The result is under `blank_refresh` in the summary\n(`updated`, `kept_existing`, `vendor_has_no_image`, `colour_not_on_blank`,\n`not_integrated`, `vendor_error`); report its counts. Non-integrated blanks\n(house brands, other vendors) stay `not_integrated` → the colour is still skipped\n`blank_color_has_no_image` for a human. A `--dry-run` only reports what it would\nfill. `--no-refresh-blanks` turns it off.\n\n`--include-imaged` also redoes colours (and the template) that already carry an\nimage — use it **only when told to**, e.g. when the store-build wizard copied the\nblank's photo onto the end items. Without it, existing images are never touched.\n\nOutput (abridged):\n\n```json\n{\"counts\": {\"image_written\": 5, \"needs_adjustment\": 1, \"failed\": 1},\n \"skipped\": {\"blank_color_has_no_image\": 2, \"already_imaged\": 7, \"unmapped_location\": 1},\n \"not_imaged\": [{\"product_tmpl_id\": 25016, \"name\": \"…\", \"reason\": \"unmapped_location\",\n   \"decorations\": [{\"decoration_id\": 143, \"location_name\": \"Left Hip\", \"reason\": \"unmapped_location\"}]}],\n \"remaining_unscanned\": 0,\n \"jobs\": [\n  {\"job\": \"24921-black\", \"status\": \"image_written\", \"art_sources\": {\"8934\": \"dtf_png\"}},\n  {\"job\": \"24921-orange\", \"status\": \"needs_adjustment\", \"attempts\": 1,\n   \"issues\": [{\"decoration_id\": 8934, \"code\": \"too_high\", \"detail\": \"…\"}],\n   \"suggested\": {\"8934\": {\"offset_y_pct\": 5}},\n   \"next\": \"python3 scripts/web_mockup_job.py adjust --job 24921-orange --apply-suggested\"},\n  {\"job\": \"24920-navy\", \"status\": \"failed\", \"error\": {\"code\": \"art_unusable\", \"detail\": \"…\"}}\n ]}\n```\n\nEach progress line is one finished colour:\n`[i/N] <job> <status> <reason or size> a<attempt> $<vision cost>`.\nThe final JSON also carries:\n\n- `cost` — vision spend for this invocation: `vision_usd`, `vision_calls`,\n  `per_written_colour_usd` (OpenRouter's exact per-call cost). Your own model's\n  spend is not included — it's on the OpenRouter dashboard.\n- `tuning` — per (category, placement): how many approved mockups needed a\n  nudge, the deltas they needed, and — when ≥ 2 agree — a suggested\n  `edit_placement_rule.py update …` command. **Report it; never run it** — a\n  default change affects every future mockup and is a human decision.\n\n### 3. Act on each `needs_adjustment`\n\nRun the job's `next` command — that's the \"easy fix\" path:\n\n```bash\npython3 scripts/web_mockup_job.py adjust --job 24921-orange --apply-suggested\n```\n\nIt layers the reviewer's deltas onto the previous attempt (halving any that flip\nsign — overshoot guard), re-composes, re-reviews, and writes on \"yes\". Repeat\nwhile it returns `needs_adjustment`; the script stops at 3 attempts\n(`rejected`). If `next` says the issue is not fixable by moving\n(`background_visible`, `bad_blank`, `art_damaged`), leave it.\n\nBefore the vision check, a geometry check also rejects a chest print whose\ncentre is outside its zone (left chest 60–72% of the garment width, right chest\n28–40%) with `outside_chest_zone` and a fix back toward the middle of the zone.\n\nManual nudge (only if you have a specific reason beyond the suggestion):\n\n```bash\npython3 scripts/web_mockup_job.py adjust --job <job> --decoration-id <id> \\\n    --offset-y-pct 4 --offset-x-pct -3 --width-delta-pct 10\n# offset-x: + = toward the IMAGE's right. offset-y: + = down. width: + = bigger.\n```\n\n`review_error` (the vision call failed) → `adjust --job <job>` with no deltas\nre-reviews once. Still failing → report it.\n\n**A person sent a written image back** (a `get_feedback` redo, or a review\nverdict with a correction): turn their words into deltas and rewrite it —\n\n```bash\npython3 scripts/web_mockup_job.py adjust --job <job> --redo --offset-x-pct -3   # \"move it toward the zipper\"\n```\n\n`--redo` needs at least one of `--offset-x-pct` / `--offset-y-pct` /\n`--width-delta-pct` (image coordinates: + x = image right, + y = down,\n+ width = bigger). The person is the reviewer, so there is no vision call and\nno adjustment cap; the geometry checks still apply. It rewrites every size of\nthe colour (and the template image if this colour supplied it). Then\n`submit_for_review` again with `revision_of` set to the review id.\n\n### 4. Report\n\nOne outcome per job plus a run summary. Use the script's statuses verbatim:\n\n| Status / skip | Meaning |\n|---|---|\n| `image_written` | Reviewed \"yes\", uploaded to every size variant (and the template if it had no image), verified byte-exact. |\n| `needs_adjustment` | Reviewer said no with a fix — run `next`. |\n| `rejected` | 3 attempts, still no — left for a human. Include the last issues. Also `error.code: adjustment_limit` when the reviewer's fixes would move a print further from its measured rule than allowed (±4 across, ±5 down, ±15% size, in total) — the rule and the reviewer disagree, so a human decides. Never work around it with manual deltas. |\n| `failed` | `error.code` says why (`art_unusable`, `background_not_removed`, `blank_looks_like_placeholder`, `source_empty`, `upload_truncated`, …). A Python error code (`TypeError`, `KeyError`, …) is a bug in the script — report `error.where` verbatim. |\n| `review_error` | Vision call failed — re-review once, then report. |\n| `blank_rejected` | The reviewer rejected the **blank photo itself** (`bad_blank`: an \"image not available\" graphic stored on the variant, an on-model shot, a different product). No nudge fixes that, so it is never `needs_adjustment`. The run automatically asks the vendor for that exact colour (`blank_refetch` in the result) and re-images it on the new photo; `blank_rejected` in the final result means the vendor had nothing better — a human must add a flat front photo. |\n| skipped `already_imaged` | Colour already has its own image. Never overwritten. |\n| skipped `blank_color_has_no_image` / `no_blank_for_color` / `blank_needs_flatfront` | The blank has no usable photo for that colour, and the automatic vendor refresh couldn't supply one (see `blank_refresh`: not integrated, or the vendor has no image) — a human needs to add it. |\n| skipped `back_only_no_back_blank` | Only back prints; blanks carry front photos only. |\n| skipped `unmapped_location` / `no_art` / `no_usable_decoration` | Nothing on the product can go on the front photo: a location name `locations.py` doesn't recognise, or no art. The product and each decoration's `location_name` + reason are listed in `not_imaged` — report them. A new location name is a skill change (`scripts/locations.py`), not an Odoo change. |\n\nSummary line: `site=<id> written=<w> adjusted=<a> rejected=<r> failed=<f> skipped=<s> remaining=<remaining_unscanned> vision=$<cost.vision_usd>`.\nThen list any `tuning` rows with `needed_adjustment > 0` (and their `suggest`).\nNothing to do is a `nothing_to_do` verdict — the steady state.\n\n### 5. Escalate what needs a person — do not just report it\n\nThe result (and `status`) carries **`needs_human`**: every job that ended\n`rejected`, `blank_rejected`, `failed`, `review_error` or `unreviewed`, any\n`needs_adjustment` with no automatic fix, and every product in `not_imaged`,\neach with `why` and the `action` a person has to take. After step 3's\nfollow-ups, re-read `python3 scripts/web_mockup_job.py status` and, if\n`needs_human` is non-empty, **call `escalate_to_human` once for the run**\n(kind `blocker`) before you finish:\n\n- `title`: `Store <site_id> mockups: <n> need a person`\n- `context`: one lead sentence, then a table — product, colour, status, why,\n  action — one row per `needs_human` item.\n- one question: \"What should I do with these?\" — options\n  `Retry after I fix them (Recommended)` (you then run\n  `run --product-tmpl-ids <ids> --retry-stuck` for those products) and\n  `Leave them parked`.\n\nThen end your turn; the answer wakes you. A run whose only open items are\npeople's work is **not** complete until it is escalated — a line in the\nsummary is not an escalation. Never escalate mid-run, and never for items\nthe script is still handling (`needs_adjustment` with a `next` command).\n\n`python3 scripts/web_mockup_job.py status` shows everything waiting on you,\nall-time vision spend (`cost_all_time`), the cumulative `tuning` table, and the\nlast 20 progress lines (`--tail N`).\n\nJobs that ended `rejected`, or `failed` on bad source data (`art_unusable`,\n`background_not_removed`, `blank_looks_like_placeholder`, `source_empty`, …), are\n**parked**: later fires report them but don't re-run them. Once a human fixes\nthe art or blank, `run … --retry-stuck` restarts them.\n\n## What the pipeline does (for debugging, not for you to redo by hand)\n\n- **Plan** (`web_mockup_plan`, server-side): per colour the size variants to\n  write, whether they already carry their *own* image (`vendor_image_url` →\n  `image_url` → `image_variant_1920`; never `image_1920`, which falls back to the\n  template), and the matching blank photo — matched by colour **name**, on-model\n  vendor shots rejected. Per decoration: the raw `location_name`, method, size\n  and art sources best-first. Odoo makes no placement decision.\n- **Placement** (`scripts/locations.py`, in the skill): location name →\n  `placement` / `view` / `image_side`; front decorations become layers, back\n  prints and unrecognised names are omitted (the blank photo is a front), and a\n  product with no front layer lands in `not_imaged` with the reason.\n- **Fetch**: everything with a `fetch` ref goes through `GET <ODOO_MCP_URL>/image`,\n  which 404s on an empty field instead of returning a placeholder — including\n  the embroidery/DTF production PNGs, which Odoo pulls from the CDN (the agent\n  host cannot reach the CDN; a direct download times out and the run falls back\n  to the thumbnail). Vendor blank URLs are fetched directly. Blanks are also\n  rejected locally if tiny or near-uniform.\n- **Art prep**: transparent production PNG → trimmed and used; opaque art →\n  plate colour-keyed (or segmented with rembg), then verified: a surviving\n  opaque border = rejected, next source.\n- **Compose**: all front layers onto one garment bbox via `compose_mockup.compose`\n  with the orientation-guarded rules; category inferred from the blank name.\n- **Checks**: geometry (image side, inside garment) → vision review\n  (`vision_review.py`, JSON only). Geometry failures never spend a vision call.\n- **Write**: 1200 px JPEG → `POST <ODOO_MCP_URL>/upload` with\n  `record_ids=<all sizes of the colour>`; `bytes_received` must equal the file\n  size and every record must report `bytes_stored`.\n\nState lives in `$MOCKUP_DATA_DIR/web_jobs/` (per-job folder + `state.json`).\n\n## The routine prompt (paste into the Mockup Artist Agent's Routines page)\n\n- **Name:** `General Purpose Web image generation routine`\n- **Schedule:** hourly during business hours (or as configured)\n- **Prompt:**\n\n  ```\n  Image new storefront products. Follow references/web_image_routine.md in the\n  drivethru-graphic-artist skill exactly — especially its Hard rules.\n\n  1. storefront_list_sites state=draft. For each site (newest first), start\n     the run IN THE BACKGROUND (see \"2. Run\" in the doc):\n       nohup python3 scripts/web_mockup_job.py run --site-id <id> --limit 10 > /dev/null 2>&1 &\n     then poll `python3 scripts/web_mockup_job.py status --brief` about once a\n     minute, posting a progress update with active_run.done/total and\n     active_run.latest each time it changes. When finished, read\n     `status --last-run`. Stop starting new sites once 30 colour jobs have run\n     this fire.\n  2. For every job with status needs_adjustment, run its \"next\" command\n     (adjust --job <job> --apply-suggested) until it is image_written or\n     rejected. For review_error, re-run `adjust --job <job>` once.\n  3. Report one line per job (status + reason), the summary line with the\n     vision cost, and any tuning rows that needed adjustment.\n  4. Re-read `status`. If `needs_human` is non-empty, call escalate_to_human\n     once (kind blocker) with a table of those items (product, colour,\n     status, why, action) — see \"5. Escalate\" in the doc. Do not finish the\n     run with them only in the summary.\n\n  Never open, Read, or view an image. Never call decoration_get_image,\n  decoration_set_image, or product_write/product_create with an image field,\n  and never pass include_binary. Never write image bytes yourself or in chunks —\n  the script does the upload. Never ask a human mid-run — escalate once, at\n  the end (step 4).\n  ```\n\nFile v0.18.1:skill-card.md\n\n## Description:\n\nCreates reviewed garment mockups, cleans up flat artwork, and prepares DTF decoration files for production.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[zmtucker](https://clawhub.ai/user/zmtucker)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDesign and merchandising teams use this skill to place artwork on product photos, review mockups, restore degraded flat art, and prepare DTF files and decoration records for printing.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Scheduled jobs and production workflows can change storefront images and decoration records.\n\nMitigation: Run only where catalog edits are authorized, with tightly scoped Odoo credentials; review proposed changes and job outcomes.\n\nRisk: The web-image routine fetches external image URLs without URL validation.\n\nMitigation: Restrict outbound network access and avoid unattended web-image runs until URL validation or an egress allowlist is in place.\n\nRisk: Independent mockup review sends images to a configured vision provider.\n\nMitigation: Choose the provider deliberately and confirm that image sharing is permitted before enabling review.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/zmtucker/skills/drivethru-graphic-artist)\n- [Decoration specifications](references/decoration_spec.md)\n- [Production-ready workflow](references/production_ready.md)\n- [Web-image routine](references/web_image_routine.md)\n- [rembg background-removal project](https://github.com/danielgatis/rembg)\n\n## Skill Output:\n\n**Output Type(s):** [PNG image files, JSON receipts, Guidance]\n\n**Output Format:** [PNG mockups and production artwork, JSON job results, and brief text guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Production artwork is sized for its print location at 300 DPI; mockups require review before delivery.]\n\n## Skill Version(s):\n\n0.18.1 (source: release metadata and skill frontmatter)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.18.0: 30 files, 5231875 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2400b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_feedback.md (2201b), references/location_dimensions.json (6095b), references/mockup_routine.md (10012b), references/placement_rules_schema.json (2533b), references/production_cleanup.md (9361b), references/production_ready.md (11128b), references/self_review.md (7035b), references/web_image_routine.md (18595b), scripts/_bootstrap.py (4631b), scripts/_paths.py (1998b), scripts/cleanup_art.py (18129b), scripts/compose_mockup.py (23131b), scripts/detect_garment_bbox.py (3208b), scripts/edit_placement_rule.py (11777b), scripts/extract_colors.py (3995b), scripts/knockout_color.py (16869b), scripts/locations.py (4730b), scripts/prepare_dtf_production.py (5835b), scripts/remove_background.py (3357b), scripts/thumbnail_card.py (4403b), scripts/upload_production_file.py (6332b), scripts/vision_review.py (17979b), scripts/web_mockup_job.py (54482b), skill-card.md (2128b), SKILL.md (30559b), _meta.json (144b)\n\nFile v0.18.0:SKILL.md\n\n---\nname: drivethru-graphic-artist\ndescription: Graphic-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.\nversion: 0.18.0\nemoji: 🎨\nmetadata:\n  openclaw:\n    requires:\n      bins: [python3]\n    envVars:\n      MOCKUP_DATA_DIR:\n        required: false\n        description: >\n          Directory for the editable placement-rules catalog and rendered\n          mockup outputs. Defaults to `~/.drivethru/mockup`. The bundled\n          starter catalog (assets/placement_rules.json) is used read-only\n          until the first edit, which seeds an editable copy here.\n      MOCKUP_VISION_API_KEY:\n        required: false\n        description: >\n          API key for the independent vision review in the web-image routine\n          (web_mockup_job.py / vision_review.py). Falls back to\n          OPENROUTER_API_KEY. Without one, mockups are never written.\n      MOCKUP_VISION_MODEL:\n        required: false\n        description: >\n          Vision model slug for the review (default anthropic/claude-sonnet-5\n          on OpenRouter). MOCKUP_VISION_BASE_URL overrides the\n          OpenAI-compatible endpoint (default https://openrouter.ai/api/v1).\n    install:\n      uv:\n        - Pillow>=10.3,<12\n        - numpy>=1.24,<3\n        - rembg>=2.0.56,<3\n        - onnxruntime>=1.18,<2\n        - scipy>=1.10,<2\n        - opencv-python-headless>=4.8,<6\n        - requests>=2.31,<3\n---\n\n# Drivethru Graphic Artist — Product Mockups\n\nTake a **blank** product photo plus a **decoration** image and return a\ncomposite **mockup**. Compositing is deterministic image manipulation: Pillow\nfor transform/compose, [rembg](https://github.com/danielgatis/rembg) (U²-Net\nsegmentation — *not* generative) for background removal and garment bbox\ndetection.\n\n**No pixels are ever model-generated.** The compositing pipeline never invokes\na generative model — only fall back to an image model to *create* artwork if\nthe user *explicitly* asks (e.g. \"generate a new logo\"), and say so first.\n\n**But you must review your own output.** After composing, you (the model\nrunning this skill) `Read` the rendered PNG, judge the placement, and\nre-compose with corrective deltas if it's off — up to 3 attempts — before\nreturning it. This is judgment, not generation: it only tunes the same numeric\nflags a human would. See [Self-review loop](#self-review-loop-required) below.\n\n## The three inputs\n\n1. **Blank** — photo of the product (t-shirt, hoodie, hat, mug, …). Any\n   resolution or crop; the garment's bounding box is detected automatically.\n2. **Decoration** — the logo/graphic to place. PNG with transparency is ideal.\n   For an opaque image, pre-clean it first: a **flat logo on a solid color**\n   should go through `knockout_color.py` (crisp edges, no halo — see\n   [Background removal](#background-removal-flat-art-vs-photos)); `--auto-remove-bg`\n   runs rembg inline and is meant for a *photographic* decoration, not flat art.\n3. **Placement** — where it goes: `full_front`, `left_chest`, `right_chest`,\n   `center_chest`, `left_sleeve`, `right_sleeve`, `left_leg`, `right_leg`\n   (front of that leg on shorts/pants), `full_back`, `back_yoke`, `front`, … (left/right = the **wearer's** — see\n   [Left / right means the WEARER's](#left--right-means-the-wearers)).\n\n**Category** (hoodie / tee / hat / mug / …) is helpful but optional. If the\nuser doesn't say, infer it from the image or chat, or omit it to fall back to\nthe `_defaults` rules.\n\n## How placement works\n\nPlacement rules are **ratios against the detected garment bounding box**, not\nabsolute pixels or inches, so a youth tee and an adult tee get visually\nmatching prints. Each rule has `width_ratio`, `x_center_ratio`, `y_top_ratio`,\n`rotation_deg`, and an optional `max_height_ratio` (caps print height to fit a\nlocation's height box — e.g. a hoodie full-front print must clear the pocket).\nLook-up order: `(category, placement)` → `(_defaults, placement)` → error.\n\nThe shipped ratios are reconciled to real industry print dimensions — see\n[`references/decoration_spec.md`](references/decoration_spec.md) (with the\ncanonical placement diagram at `assets/decoration_guide.png`). That's the\nground truth the self-review loop judges against: a chest logo is pocket-sized\n(~¼ the width of a full front), a full back equals a full front, etc.\n\nThe catalog ships with the skill at `assets/placement_rules.json` (read-only\nstarter). When the agent adds or refines rules, an editable copy is created in\nthe data dir (`$MOCKUP_DATA_DIR` or `~/.drivethru/mockup`) and persists there.\nSee [`references/placement_rules_schema.json`](references/placement_rules_schema.json)\nfor the exact schema.\n\n## Requirements\n\n- `python3`, plus `uv` on PATH (used to self-bootstrap dependencies).\n- **Dependencies install themselves — you never have to `pip install` by hand.**\n  Each script ensures its own imports at startup: if `Pillow`/`numpy`/`rembg`\n  aren't already importable, it builds a cached venv in the data dir\n  (`$MOCKUP_DATA_DIR/.venv`, default `~/.drivethru/mockup/.venv`) with `uv` and\n  re-execs into it. Hosts that honor the frontmatter `install.uv` pre-install\n  everything and the bootstrap is a no-op; otherwise the first run pays a short\n  one-time install. See [`scripts/_bootstrap.py`](scripts/_bootstrap.py).\n- Three dependency tiers, so the common flat-art path stays cheap:\n  - **light** — `Pillow` + `numpy`. Everything except segmentation and cleanup\n    (`knockout_color.py`, `prepare_dtf_production.py`, `extract_colors.py`).\n    Installs in ~2 s, no model download.\n  - **cleanup** — adds `scipy` + `opencv-python-headless` (a few tens of MB of\n    wheels, **no** model download). Only `cleanup_art.py` pulls it in — scipy for\n    the despeckle/close morphology, headless OpenCV for the corner-preserving\n    vector trace. Works fully offline. (`--method raster` skips OpenCV and needs\n    only scipy.)\n  - **heavy** — adds `rembg` + `onnxruntime` (~170 MB of wheels **plus** a\n    one-time ~170 MB `u2net` model download on first segmentation). Only\n    `remove_background.py`, `detect_garment_bbox.py`, and `compose_mockup.py`\n    pull it in. The model download needs outbound network once; if it's blocked,\n    bbox detection falls back to the full image frame and `--auto-remove-bg`\n    errors — but the color-key path (`knockout_color.py`) still works offline.\n\n## Scripts\n\n| Script | Purpose |\n|---|---|\n| `scripts/compose_mockup.py` | The workhorse: detect bbox, look up rule, scale/rotate/paste, write PNG, print a JSON receipt. |\n| `scripts/detect_garment_bbox.py` | Standalone: print the garment bbox JSON for a blank. |\n| `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](#background-removal-flat-art-vs-photos). |\n| `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. |\n| `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. |\n| `scripts/edit_placement_rule.py` | Schema-validated, atomic mutator for `placement_rules.json` (`show` / `add` / `update` / `remove`). |\n| `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`](references/production_cleanup.md). |\n| `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. |\n| `scripts/extract_colors.py` | Production-ready: extract dominant colors from art as `#RRGGBB` + coverage (feeds `decoration_match_colors`). |\n| `scripts/locations.py` | Decoration location name → wearer-side `placement` / `view` / `image_side` (legs included), and which decorations layer onto the front photo. Odoo sends raw names; this is where they are interpreted. |\n| `scripts/web_mockup_job.py` | **Web-image routine, end to end:** plan (`web_mockup_plan`) → fetch → art prep → compose → geometry + vision checks → verified upload to every size variant. JSON out only (plus one live progress line per colour on stderr); reports vision `cost` and placement `tuning`. `run` / `adjust` / `status`. See [`references/web_image_routine.md`](references/web_image_routine.md). |\n| `scripts/vision_review.py` | Independent vision QA of a rendered mockup → `{verdict: yes/no, checks, issues[{fix}], usage{cost_usd}}`. Sends the image to a vision model over HTTP so it never enters the agent's context. Used by `web_mockup_job.py`. |\n| `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. |\n\n## Composing a mockup\n\n```bash\npython3 scripts/compose_mockup.py \\\n    --blank /path/to/blank.jpg \\\n    --decoration /path/to/logo.png \\\n    --category hoodie \\\n    --placement full_front \\\n    [--auto-remove-bg] \\\n    [--width-delta-pct 0] [--offset-x-pct 0] [--offset-y-pct 0] \\\n    [--rotate-deg 0] \\\n    [--output /path/to/out.png]\n```\n\nThe script prints JSON with the detected `garment_bbox`, the resolved `rule`,\nthe `applied` ratios/deltas, and the `output` path. Return the PNG to the user\nand add one line in human terms (\"55% of the garment width, centered on the\nchest, no rotation\").\n\nDefaults: rules come from the editable data-dir copy if present, else the\nbundled starter; output goes to `<data dir>/out/<uuid>.png`. Override with\n`--rules` / `--output`.\n\n## Self-review loop (required)\n\nThe ratio rules are a *starting guess*. For a given garment/decoration pair\nthey can land the print too high, too small, or off-center — and the\ndeterministic pipeline can't notice, because it has no eyes. You do. **Do not\nreturn the first compose unseen.**\n\nAfter every compose, run this loop before handing anything to the user:\n\n1. **Compose** with the current flags; note the `output` path from the receipt.\n2. **`Read` the output PNG** — actually look at the rendered mockup.\n3. **Judge** it against what the placement should look like (centered on the\n   chest for `full_front`, small over the pec for `left_chest`, etc.).\n4. If it looks good → return it. If it's off and you have attempts left →\n   derive corrective deltas and re-compose, **layering them on the previous\n   run's flags** (same deltas as the iterative-feedback table: e.g. print\n   riding too high → `--offset-y-pct +8`; too small → `--width-delta-pct +12`).\n5. **Hard cap: 3 compose attempts.** If attempt 3 still isn't great, return the\n   best one and tell the user in one line what's still off and offer to keep\n   tuning. Never loop past 3, and never ping-pong a delta's sign — if you\n   overshoot, you're close; accept the better result.\n\nThe full review checklist (per-placement targets, critique→delta mapping,\noscillation/no-progress guards, what to tell the user) is in\n[`references/self_review.md`](references/self_review.md), and the targets there\nare anchored to real print dimensions in\n[`references/decoration_spec.md`](references/decoration_spec.md). This runs\nentirely in-container — you are the reviewer; there is no separate model call.\n\n## Iterative feedback\n\nMockups are a back-and-forth. When the user says \"bigger\", \"move it up\",\n\"rotate it\", layer deltas on top of the **previous** run's args (e.g.\n`--width-delta-pct +10`, `--offset-y-pct -5`, `--rotate-deg 5`). Keep a running\nrecord of the current flags so each turn builds on the last. The full\nfeedback→flags mapping and how to promote a tuned result into a saved default\nare in [`references/iterative_feedback.md`](references/iterative_feedback.md).\n\n## Cleaning up degraded / AI-generated art\n\nSome art that comes in isn't clean vector — it's **\"fake vector\":** a logo an\nimage model generated (or someone upscaled from a tiny JPEG) that *looks* crisp\nat thumbnail size but is a soft raster full of **ghosting, desaturated haze,\nfaded/broken outlines, jagged uneven strokes, and blurry edges.** You don't see\nit in the DB thumbnail; it's glaring once it prints at **13″**. Catching and\nfixing that is part of \"dropping for production.\"\n\n**The move is to restore, not regenerate.** A human artist wouldn't salvage the\nblurry pixels or feed it to an image generator (which would rewrite the\nletterforms and text) — they'd rebuild it as the few flat inks it was always\nmeant to be. `scripts/cleanup_art.py` does exactly that, **deterministically (no\nmodel-generated pixels):** snap every pixel to its true ink (dropping the\nhaze/ghost), despeckle + close each ink mask to rejoin broken outlines, and\nre-render each mask crisp at print size with a **corner-preserving vector trace**\n(smooths the jaggies/waviness without rounding letter corners).\n\n```bash\npython3 scripts/cleanup_art.py --input /tmp/thumb.png \\\n    --inks '#26296B,#A0202C,#FFFFFF' \\     # decoration colors[].rgb_hex (best source)\n    --output /tmp/thumb_clean.png\n```\n\nIt writes the cleaned PNG **plus a `*_proof.png`** (before/after on gray + dark,\nand a zoom on the outline detail where defects hide) and a JSON receipt with a\n`review_checklist`. **`Read` the proof and self-review it** — outlines crisp and\nuniform? corners still sharp? letterforms unchanged? ghost/haze gone? — then tune\nand re-run if needed. Same eyes-on discipline as the mockup\n[self-review](references/self_review.md); the customer sees the reviewed result.\n\nThis restores *intent*, it doesn't redesign: it keeps intended style (e.g. hollow\noutline letters stay hollow), preserves counters, and **can't invent a mark\nthat's genuinely missing** (only faded/broken ones) — flag those for a redraw.\nFull procedure, the defect-recognition guide, tuning table, and the\nfaithful-vs-restyle line: **[`references/production_cleanup.md`](references/production_cleanup.md).**\n\n## Making art production-ready (DTF) — \"dropping\" the art\n\nSeparate from mockups: when the user gives you a **decoration id** and asks to\nmake it **production-ready** / **drop the art** / **get it to done**, you take the\ndecoration's real thumbnail, size it to its location, render it at **300 DPI**,\nupload the DTF production file, set the actual size + colors, and create a print\nsample — driving the record toward the `done` state. **First inspect the art at\nproduction scale** — if it's degraded / AI-generated (ghosting, haze, faded or\njagged outlines), clean it up with `cleanup_art.py`\n([above](#cleaning-up-degraded--ai-generated-art)) *before* sizing it. This is deterministic image\nwork (the same *no model-generated pixels* rule applies) and almost always\napplies to decorations whose method is **DTF**.\n\nYou drive it through the `drivethru_mcp` **`decoration_*`** MCP tools\n(`decoration_get_production_readiness` → `decoration_get_image` →\n`decoration_set_image` → `decoration_update_fields` → `decoration_match_colors` →\n`decoration_create_sample` → `decoration_set_state`) plus the two scripts\n`prepare_dtf_production.py` and `extract_colors.py`.\n\n> **How to call these:** the `decoration_*` tools are MCP tools your host\n> already exposes — invoke each one **directly as a tool call**, like any other\n> tool available to you. Do **not** hand-roll an HTTP request to the Odoo host to\n> *invoke* a tool, and do **not** go hunting for an `ODOO_MCP_URL` / `ODOO_MCP_TOKEN`\n> to POST against for that: there is **no `/call` REST route**, so a 404 there\n> means you invented a URL, not that the MCP is down.\n>\n> Raw HTTP has exactly **two** legitimate uses here — both for moving image\n> *bytes* out of your token stream, never for invoking a tool: **downloading** a\n> `cdn_url` that `decoration_get_image` returns for a large/offloaded binary\n> (step 2), and **uploading** a real production file with `upload_production_file.py`\n> (step 5, when `ODOO_MCP_URL` / `ODOO_MCP_TOKEN` are set). Everything else is a\n> tool call.\n\nStart with `decoration_get_production_readiness` — it returns a `blocking_gaps`\nlist mirroring Odoo's own `done` gate (production file, size, colors, sample,\nand a completed linked design). Per-location max print sizes come from the Odoo\n`decoration_location` record, with [`references/location_dimensions.json`](references/location_dimensions.json)\n(built from the official spec sheet, `references/decoration_spec_sheet.pdf`) as\nthe fallback.\n\nNote: a decoration can't actually reach `done` without a linked `design` in the\n`done` state — expect that gap to remain and report it, after completing every\nother step and creating the ready sample.\n\n**Full step-by-step procedure: [`references/production_ready.md`](references/production_ready.md).**\n\n## Batch mockup routine (Mockup Artist Agent)\n\nSeparate from the interactive workflow: the **Mockup Artist Agent** runs a\nscheduled routine that sweeps Odoo for open decoration requests **assigned to\nZach Tucker**, and — for each one where both a **blank product image** and a\n**decoration image** are already attached — generates a mockup and writes it\ninto the request's `mockup_image` field. Requests missing an input, or that\nalready have a mockup, are recorded as `skipped` and left alone. Rendering is\nthe same deterministic pipeline (`compose_mockup.py` + the mandatory\n[self-review loop](#self-review-loop-required)); this routine only wraps it\nin a per-request loop plus the Odoo read/write plumbing (dotted-path search\nacross `sale.order.decoration_request_ids`, attachment listing, base64 write\nvia `decoration_set_image`).\n\n**Full step-by-step procedure, per-outcome logging, and the exact routine\nprompt/cron to paste into the agent's Routines page:\n[`references/mockup_routine.md`](references/mockup_routine.md).**\n\n## Web-image routine — per-color storefront mockups (Mockup Artist Agent)\n\nA second scheduled routine — **\"General Purpose Web image generation\nroutine\"** — images the **storefront catalog**: products built in bulk from\nblanks + decorations when a store is created, which arrive with no image. For\neach colour it composites the product's front decoration(s) onto that colour's\nblank and writes the mockup to every size variant of the colour.\n\n**It is one command — `scripts/web_mockup_job.py run --site-id <id>` — and it\nis different from the interactive flow above in three ways that matter:**\n\n- **Do not `Read` or open any image in this routine.** The self-review is done\n  by an independent vision model inside the script (`vision_review.py`), which\n  returns a yes/no verdict as JSON. Images in your context balloon the session.\n- **Do not fetch or write images yourself** — no `decoration_get_image`, no\n  `include_binary`, no `product_write` image fields, no chunked\n  `decoration_set_image`. The script downloads over HTTP and uploads byte-exact\n  through `POST <ODOO_MCP_URL>/upload`, verified.\n- **A \"no\" verdict comes with a fix:** run the job's `next` command\n  (`web_mockup_job.py adjust --job <job> --apply-suggested`). Max 3 attempts.\n\nBlank presence and art choice (embroidery/DTF production PNG before the\nthumbnail, whose background is removed and verified) are resolved server-side\nby the `web_mockup_plan` MCP tool, which returns each decoration's raw location\nname. Wearer-side placement is decided in the skill by `scripts/locations.py`.\n\n**Full procedure, hard rules, statuses, and the exact routine prompt:\n[`references/web_image_routine.md`](references/web_image_routine.md).**\n\n## Left / right means the WEARER's\n\nEvery placement name — `left_chest`, `right_chest`, `left_sleeve`,\n`right_sleeve` — refers to the **wearer's** side. Blanks are flat **front**\nphotos, so the wearer's left is on the **image's right** (`x_center_ratio >\n0.5`) and the wearer's right is on the image's left. The bundled rules follow\nthis; `compose_mockup.py` mirrors (and warns about) any rule that doesn't, and\n`edit_placement_rule.py` refuses to save one. `--offset-x-pct` is always in\nimage coordinates: positive moves toward the image's right. Plain `sleeve` is\nrejected as ambiguous.\n\n## Editing the rules catalog\n\nShow the current catalog:\n\n```bash\npython3 scripts/edit_placement_rule.py show [--category hoodie] [--placement full_front]\n```\n\nAdd a new category/placement when one is missing:\n\n```bash\npython3 scripts/edit_placement_rule.py add tote front \\\n    --width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30\n```\n\nRefine an existing default (e.g. after the user approves a tuned result):\n\n```bash\npython3 scripts/edit_placement_rule.py update hoodie full_front --width-ratio 0.58\n```\n\nEdits are validated and written atomically to the editable copy in the data dir\n(seeded from the bundled starter on first edit) — the shipped asset is never\nmutated.\n\n## Background removal: flat art vs photos\n\nThere are **two** background removers here because they solve different problems.\nPick by what the source *is*, not by habit:\n\n| The source is… | Use | Why |\n|---|---|---|\n| **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 |\n| **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 |\n\n**Do not use rembg (`remove_background.py`) on a flat logo.** rembg is a\nsalient-object segmentation model built for photos; on flat art it produces a\nsoft matte that leaves a light **halo**, and it has no notion of \"make *this*\ncolor transparent,\" so it can't cleanly knock out a white plate.\n\n### Knocking a color out of flat art\n\n```bash\npython3 scripts/knockout_color.py --input /tmp/logo.jpg --output /tmp/logo.png \\\n    [--mode color-to-alpha|flood] [--color auto|'#RRGGBB'] [--fuzz 0.10] [--feather 2]\n```\n\nThe key color defaults to `auto` (median of the four corners). Two modes:\n\n- **`color-to-alpha`** (default) — remove the key color **everywhere**, with\n  clean anti-aliased edges. Each pixel's alpha becomes proportional to its\n  distance from the key color and the foreground color is un-multiplied back\n  out, so a gray edge pixel becomes *semi-transparent black* instead of an\n  opaque gray rim. Best for one-color line art you want printed as ink on the\n  garment (the garment shows through letter counters and open areas).\n- **`flood`** — remove only the background **connected to the border** (a\n  tolerant flood fill, feathered at the edge). Enclosed regions of the key color\n  are **kept** — a white field inside an outline stays white. This is the\n  \"die-cut sticker\" look.\n\n#### Which mode? Decide it, don't guess it\n\nThe choice hinges on one thing: **is the key color an *ink* on this decoration,\nor just background?** If it prints → `flood` (keep it). If it's background →\n`color-to-alpha` (knock it out). Don't eyeball-guess, and don't blindly trust the\ncolor list either — **run `--analyze` first** and reconcile the two:\n\n```bash\npython3 scripts/knockout_color.py --input /tmp/thumb.png --analyze \\\n    --expect-colors '#000000'      # the decoration's declared inks (colors[].rgb_hex)\n```\n\nAnalyze writes nothing and reports:\n- the auto-detected **key color**, and whether it matches a declared ink;\n- **`enclosed_fraction`** — key-color pixels *inside* the art (letter counters, a\n  field inside an outline). This is the number that decides whether the mode even\n  matters: near-zero → both modes look the same; large → it's a real call;\n- a **`recommended_mode`** derived from the inks (key color is a declared ink →\n  `flood`; not → `color-to-alpha`), plus `warnings`.\n\n**Use the color list as a guide, then verify.** The inks are operator-entered and\ncan be stale or wrong, so treat the recommendation as a strong prior, not a\nverdict — when `decision_matters` is true, `Read` the actual cutout before\ntrusting it. Ask the user **only** when the signal is genuinely ambiguous\n(a meaningful `enclosed_fraction` **and** the inks don't resolve it, or the inks\ncontradict what you see) — show both rendered options in the question. When the\ninks clearly resolve it and the result looks right, just proceed.\n\n> Example — decoration 2878 (Bacon & Co logo): declared inks `[Black]`, so white\n> is *not* an ink → analyze recommends `color-to-alpha`, but flags that it removes\n> ~38% of the image (the interior field). Correct call (a one-color black print\n> where the garment shows through), but big enough to eyeball before uploading.\n\n#### The thumbnail's background is not a print instruction\n\nA decoration has two separate image artifacts — don't let one contaminate the\nother:\n\n| | `image` (thumbnail) | `dtf_production_png` (production file) |\n|---|---|---|\n| For | humans browsing the DB | the printer |\n| Background | **keep one** — legibility wins | **none** — true cutout on transparency |\n| Decides \"does white print?\" | never | its alpha, set from the color list |\n\nSo a thumbnail sitting on a white plate tells you **nothing** about whether that\nwhite should print — that's what `--expect-colors` is for. **Do not assume\nthumbnails have any particular background**; today they carry whatever was\nsupplied (a plate, a photo backdrop, or nothing), and migrating them is a slow,\nseparate effort. Point the knockout at the thumbnail regardless and let the color\nlist drive the cutout. If a thumbnail is itself hard to see (white art on a white\nUI), fix the *thumbnail* with `thumbnail_card.py` (composite onto a neutral gray\nor checker card) — never by baking a background into the art or the production\nfile.\n\nTwo failure modes this replaces — both from treating flat art as a photo or as a\nhard threshold:\n\n- *\"Small halo, and white left inside the logo\"* → rembg's soft matte + no\n  interior handling. `color-to-alpha` removes all the white (interior included)\n  with no halo; `flood` keeps the interior on purpose.\n- *\"The black border got all muddied up\"* → a hard \"make white transparent\"\n  threshold leaves the anti-aliased gray edge pixels fully opaque, so the smooth\n  edge turns into a dirty jagged rim. `color-to-alpha` ramps those edge pixels'\n  alpha instead, keeping the border crisp.\n\n### Segmenting a photographic subject (rembg)\n\n```bash\npython3 scripts/remove_background.py --input /tmp/photo.jpg --output /tmp/cut.png\n```\n\nIf the input already has meaningful transparency it is copied through unchanged\n(`{\"skipped\": true}`); pass `--force` to re-run rembg anyway.\n\n## Rules to follow\n\n- **No model-generated pixels** in the compositing *or* cleanup pipeline unless\n  the user explicitly asks — and say so first. Cleaning up degraded art means\n  restoring its existing inks deterministically, **not** regenerating it (a\n  generative model would rewrite the letterforms/text). Reviewing your own output\n  is fine and required — that's judgment, not generation.\n- **Inspect art at production scale before a drop.** A thumbnail hides ghosting,\n  haze, and faded/jagged outlines that wreck a 13″ print. If the art is degraded\n  or AI-generated, clean it with `cleanup_art.py` and `Read` the proof before\n  sizing — see [Cleaning up degraded art](#cleaning-up-degraded--ai-generated-art).\n- **Always self-review before returning.** `Read` the rendered PNG and\n  re-compose with deltas if the placement is off, up to 3 attempts. See\n  [Self-review loop](#self-review-loop-required).\n- **Respect aspect ratio.** `compose_mockup.py` locks it automatically; never\n  hand it raw pixel dimensions that would squash the decoration.\n- **Don't assume a file exists.** Verify input paths before composing.\n- **Save every mockup** so you can diff between iterations.\n- **Lead with the reviewed result** (the PNG), then one line on the placement\n  (and any auto-adjustment you made), then ask what to tune next.\n- When ambiguous (\"put it on the chest\"), ask one clarifying question\n  (\"full front or left chest?\") rather than guessing.\n\n## When NOT to use\n\n- The user wants a brand-new logo/design *created* from a prompt → that's\n  generative image work, out of scope here.\n- The user wants vector/print-ready separations, embroidery **DST** digitizing,\n  or color-by-color screen seps → out of scope. (Preparing a **DTF** production\n  PNG at 300 DPI *is* in scope — see [Making art production-ready](#making-art-production-ready-dtf--dropping-the-art).)\n- The user wants a photoreal render with lighting/wrinkle warping → this skill\n  does flat ratio-based compositing, not 3D/displacement warping.\n\nFile v0.18.0:_meta.json\n\n{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-graphic-artist\",\n  \"version\": \"0.18.0\",\n  \"publishedAt\": 1790272461171\n}\n\nFile v0.18.0:references/decoration_spec.md\n\n# Decoration spec — real-world print sizes & placements\n\nGround-truth print dimensions from the **Bacon & Co. Recommended Garment &\nAccessory Decoration Guide** (`assets/decoration_guide.png` — the canonical\nplacement diagram; open it when you need to see where a location sits). Use\nthis as the reference the [self-review loop](self_review.md) judges against, so\n\"correct\" means *industry-standard*, not a guess.\n\n## Why inches map cleanly to our ratios\n\nThe spec is in inches and split adult/youth; our rules are ratios of the\ndetected garment bbox. Those are two views of the same thing: youth full-front\n(9″) ÷ adult full-front (13″) ≈ 0.69, and youth garments run ~0.69 the width of\nadult — so **one ratio reproduces both columns automatically**. That's the\nwhole reason the model is ratio-based; the spec validates it. What the spec\npins down that a lone ratio can't is the *proportion between placements* (a left\nchest is ~¼ the width of a full front) and *height caps* (a hoodie front must\nclear the pocket).\n\n## Print dimensions (adult / youth), width unless noted\n\n| # | Location | Adult | Youth | Frac of full-front width |\n|---|---|---|---|---|\n| 1 | Full front | 13″W | 9″W | 1.00 (the anchor) |\n| 2 | Center chest | 3–5″W | 3″W | ~0.31 |\n| 3/4 | Right / left chest | 3–4″W | 3–4″W | ~0.27 (pocket-sized) |\n| 5 | Pocket | 3″W × 3″H | 3″W × 3″H | ~0.23, square |\n| 6 | Full back | 13″W | 9″W | 1.00 (= full front) |\n| 7 | Upper back | 10–13″W | 7–9″W | ~0.88 |\n| 8 | Tag | 3–4″W | 3–4″W | ~0.27, high under collar |\n| 9 | Lower back | 10–13″W | 7–9″W | ~0.88, low |\n| 10/11 | Sleeve (short) | 3–4″W | 3″W | ~0.27 |\n| 12/13 | Sleeve (long) | 3.25″W × **12″H** | 3″W × 9″H | ~0.25, height-dominant |\n| 14 | Full front (hoodie) | 13″W × **9″H** | 9″W × 6″H | 1.00 width, **9″H cap (pocket)** |\n| 15/16 | Left / right chest (polo) | 3–4″W | 3″W | ~0.27 |\n| 23 | Drawstring backpack | 11–12″W | — | large, centered panel |\n| 24 | Hat front | 2.5″H max (high) / 2″H (low) | — | **height-capped**, width varies |\n| 25 | Hat back | 1.25″H max | — | small |\n| 26 | Hat side | 2″W max | — | small |\n| 27 | Visor front | 1.5″H max (high) / 1″H (low) | — | small |\n\nEmbroidery floor: lettering ≥ 0.25″H and 2 mm stroke — a mark that renders\nbelow that in the mockup is too small to actually sew.\n\n## How this shaped the shipped ratios\n\n`assets/placement_rules.json` was reconciled to the proportions above:\n\n| Placement | Was | Now | Why |\n|---|---|---|---|\n| left/right chest (all cats) | 0.18 | 0.14–0.15 | 3–4″ is ~¼ of a 13″ full front — pocket-sized, not 1/3 |\n| full_back | 0.55–0.60 | 0.50–0.55 | spec sizes full back = full front (both 13″) |\n| back_yoke / upper back | 0.40 | 0.44 | spec upper back ~10–13″ ≈ 0.88 of full front |\n| sleeve | 0.12 | 0.14 | spec sleeve 3–4″ ≈ 0.27 of full front |\n| hoodie full_front | 0.55 | 0.55 + `max_height_ratio 0.33` | 13″×9″ box — cap height so tall art clears the pocket |\n\nFull front, hat, and mug were left as-is (full front already matched; hats are\nheight-capped in ways the width-ratio model doesn't govern; no mug on the\nsheet).\n\n## Caveats when reading the sheet\n\n- **Absolute inches need a bbox width to become a ratio**, and the detected\n  bbox varies with the photo (a worn hoodie's silhouette includes sleeves; a\n  flat-lay tee doesn't). So trust the *proportions between locations* over any\n  single inch→ratio conversion.\n- **\"may vary based on artwork\"** — the sheet says so itself. These are\n  recommended maxes/typicals, not hard law. The self-review loop should treat\n  them as the target to land near, then defer to what looks right on the\n  specific garment.\n- **Height caps are estimates in ratio terms.** The hoodie 9″ pocket line ≈\n  0.33 of a ~27″ body bbox; if a specific blank is cropped differently, the cap\n  may need a nudge. It only ever scales art *down* to fit, never up.\n\nFile v0.18.0:references/iterative_feedback.md\n\n# Iterative feedback & rule tuning\n\nMockups are a conversation. The first compose is a starting point; the user\nwill react (\"bigger\", \"move it up\", \"rotate it\"). Translate plain-language\nfeedback into deltas layered on top of the *previous* run's arguments.\n\n## Feedback → flags\n\n| User says | You run (added to the last run's args) |\n|---|---|\n| \"Make it ~10% bigger\" | `--width-delta-pct +10` |\n| \"Make it ~15% smaller\" | `--width-delta-pct -15` |\n| \"Move it up a little\" | `--offset-y-pct -5` |\n| \"Move it down\" | `--offset-y-pct +5` |\n| \"Shift left\" | `--offset-x-pct -5` |\n| \"Shift right\" | `--offset-x-pct +5` |\n| \"Rotate 5° clockwise\" | `--rotate-deg 5` |\n| \"Rotate 5° counter-clockwise\" | `--rotate-deg -5` |\n\nKeep a running record of the current flags so each turn composes on top of the\nlast, not from the original defaults. A delta of `±5` percentage points is a\ngood \"a little\" nudge; `±10–15%` is a good \"noticeably\" nudge for width.\n\n## Promoting a tuned result to a default\n\nWhen the user is happy and says something like \"save that as the new default\nfor hoodie full-front,\" bake the *effective* ratios from the last compose\nreceipt's `applied` block into the catalog:\n\n```bash\npython3 scripts/edit_placement_rule.py update hoodie full_front \\\n    --width-ratio 0.58 --y-top-ratio 0.20\n```\n\n## Adding a brand-new category or placement\n\nWhen the user asks for a placement/category that isn't in the catalog yet:\n\n```bash\npython3 scripts/edit_placement_rule.py add tote front \\\n    --width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30\n```\n\nThen re-compose with that `--category`/`--placement`.\n\n## Coordinate model recap\n\nAll rule fields are ratios against the **detected garment bounding box**, not\nthe full image:\n\n- `width_ratio` — decoration width ÷ garment bbox width (0.01–1.5).\n- `x_center_ratio` — decoration center X within the bbox (0 = left, 1 = right).\n- `y_top_ratio` — decoration top Y within the bbox (0 = top, 1 = bottom).\n- `rotation_deg` — clockwise rotation in degrees (−180…180).\n\nBecause they're ratios, the same rule yields a visually matching print across\nresolutions, crops, and garment sizes (a youth tee and an adult tee line up).\n\nFile v0.18.0:references/location_dimensions.json\n\n{\n  \"_meta\": {\n    \"source\": \"Bacon & Co. Recommended Garment & Accessory Decoration Guide (references/decoration_spec_sheet.pdf)\",\n    \"units\": \"inches\",\n    \"note\": \"Max print dimensions per decoration location. Ranges are captured as the UPPER bound in max_width_in / max_height_in; the raw label is preserved. At runtime prefer the authoritative Odoo decoration_location.max_width/max_height for the specific record; use this table as a fa\n\nArchive v0.17.0: 30 files, 5231547 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2400b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_feedback.md (2201b), references/location_dimensions.json (6095b), references/mockup_routine.md (10012b), references/placement_rules_schema.json (2533b), references/production_cleanup.md (9361b), references/production_ready.md (11128b), references/self_review.md (7035b), references/web_image_routine.md (17887b), scripts/_bootstrap.py (4631b), scripts/_paths.py (1998b), scripts/cleanup_art.py (18129b), scripts/compose_mockup.py (23131b), scripts/detect_garment_bbox.py (3208b), scripts/edit_placement_rule.py (11777b), scripts/extract_colors.py (3995b), scripts/knockout_color.py (16869b), scripts/locations.py (4730b), scripts/prepare_dtf_production.py (5835b), scripts/remove_background.py (3357b), scripts/thumbnail_card.py (4403b), scripts/upload_production_file.py (6332b), scripts/vision_review.py (17979b), scripts/web_mockup_job.py (52918b), skill-card.md (3092b), SKILL.md (30559b), _meta.json (144b)\n\nArchive v0.15.1: 29 files, 5225863 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2398b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_feedback.md (2201b), references/location_dimensions.json (6095b), references/mockup_routine.md (10012b), references/placement_rules_schema.json (2533b), references/production_cleanup.md (9361b), references/production_ready.md (11128b), references/self_review.md (7035b), references/web_image_routine.md (14795b), scripts/_bootstrap.py (4631b), scripts/_paths.py (1998b), scripts/cleanup_art.py (18129b), scripts/compose_mockup.py (23131b), scripts/detect_garment_bbox.py (3208b), scripts/edit_placement_rule.py (11777b), scripts/extract_colors.py (3995b), scripts/knockout_color.py (16869b), scripts/prepare_dtf_production.py (5835b), scripts/remove_background.py (3357b), scripts/thumbnail_card.py (4403b), scripts/upload_production_file.py (6332b), scripts/vision_review.py (16772b), scripts/web_mockup_job.py (45979b), skill-card.md (2826b), SKILL.md (30231b), _meta.json (144b)\n\nArchive v0.15.0: 29 files, 5224445 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2141b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_feedback.md (2201b), references/location_dimensions.json (6095b), references/mockup_routine.md (10012b), references/placement_rules_schema.json (2533b), references/production_cleanup.md (9361b), references/production_ready.md (11128b), references/self_review.md (7035b), references/web_image_routine.md (14584b), scripts/_bootstrap.py (4631b), scripts/_paths.py (1998b), scripts/cleanup_art.py (18129b), scripts/compose_mockup.py (23131b), scripts/detect_garment_bbox.py (3208b), scripts/edit_placement_rule.py (11777b), scripts/extract_colors.py (3995b), scripts/knockout_color.py (16869b), scripts/prepare_dtf_production.py (5835b), scripts/remove_background.py (3357b), scripts/thumbnail_card.py (4403b), scripts/upload_production_file.py (6332b), scripts/vision_review.py (15389b), scripts/web_mockup_job.py (43806b), skill-card.md (2899b), SKILL.md (30170b), _meta.json (144b)\n\nArchive v0.14.4: 29 files, 5223589 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2141b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_feedback.md (2201b), references/location_dimensions.json (6095b), references/mockup_routine.md (10012b), references/placement_rules_schema.json (2533b), references/production_cleanup.md (9361b), references/production_ready.md (11128b), references/self_review.md (7035b), references/web_image_routine.md (13650b), scripts/_bootstrap.py (4631b), scripts/_paths.py (1998b), scripts/cleanup_art.py (18129b), scripts/compose_mockup.py (23131b), scripts/detect_garment_bbox.py (3208b), scripts/edit_placement_rule.py (11777b), scripts/extract_colors.py (3995b), scripts/knockout_color.py (16869b), scripts/prepare_dtf_production.py (5835b), scripts/remove_background.py (3357b), scripts/thumbnail_card.py (4403b), scripts/upload_production_file.py (6332b), scripts/vision_review.py (15389b), scripts/web_mockup_job.py (42226b), skill-card.md (2917b), SKILL.md (30170b), _meta.json (144b)\n\nArchive v0.14.3: 29 files, 5222476 bytes\n\nFiles: assets/decoration_guide.png (467318b), assets/placement_rules.json (2141b), references/decoration_spec_sheet.pdf (4795287b), references/decoration_spec.md (4034b), references/iterative_fe...","readmeExcerpt":"Skill: drivethru-graphic-artist Owner: zmtucker Summary: Graphic-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 ","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"python3 scripts/compose_mockup.py \\\n    --blank /path/to/blank.jpg \\\n    --decoration /path/to/logo.png \\\n    --category hoodie \\\n    --placement full_front \\\n    [--auto-remove-bg] \\\n    [--width-delta-pct 0] [--offset-x-pct 0] [--offset-y-pct 0] \\\n    [--rotate-deg 0] \\\n    [--output /path/to/out.png]"},{"language":"bash","snippet":"python3 scripts/cleanup_art.py --input /tmp/thumb.png \\\n    --inks '#26296B,#A0202C,#FFFFFF' \\     # decoration colors[].rgb_hex (best source)\n    --output /tmp/thumb_clean.png"},{"language":"bash","snippet":"python3 scripts/edit_placement_rule.py show [--category hoodie] [--placement full_front]"},{"language":"bash","snippet":"python3 scripts/edit_placement_rule.py add tote front \\\n    --width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30"},{"language":"bash","snippet":"python3 scripts/edit_placement_rule.py update hoodie full_front --width-ratio 0.58"},{"language":"bash","snippet":"python3 scripts/knockout_color.py --input /tmp/logo.jpg --output /tmp/logo.png \\\n    [--mode color-to-alpha|flood] [--color auto|'#RRGGBB'] [--fuzz 0.10] [--feather 2]"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: drivethru-graphic-artist\ndescription: Graphic-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.\nversion: 0.18.1\nemoji: 🎨\nmetadata:\n  openclaw:\n    requires:\n      bins: [python3]\n    envVars:\n      MOCKUP_DATA_DIR:\n        required: false\n        description: >\n          Directory for the editable placement-rules catalog and rendered\n          mockup outputs. Defaults to `~/.drivethru/mockup`. The bundled\n          starter catalog (assets/placement_rules.json) is used read-only\n          until the first edit, which seeds an editable copy here.\n      MOCKUP_VISION_API_KEY:\n        required: false\n        description: >\n          API key for the independent vision review in the web-image routine\n          (web_mockup_job.py / vision_review.py). Falls back to\n          OPENROUTER_API_KEY. Without one, mockups are never written.\n      MOCKUP_VISION_MODEL:\n        required: false\n        description: >\n          Vision model slug for the review (default anthropic/claude-sonnet-5\n          on OpenRouter). MOCKUP_VISION_BASE_URL overrides the\n          OpenAI-compatible endpoint (default https://openrouter.ai/api/v1).\n    install:\n      uv:\n        - Pillow>=10.3,<12\n        - numpy>=1.24,<3\n        - rembg>=2.0.56,<3\n        - onnxruntime>=1.18,<2\n        - scipy>=1.10,<2\n        - opencv-python-headless>=4.8,<6\n        - requests>=2.31,<3\n---\n\n# Drivethru Graphic Artist — Product Mockups\n\nTake a **blank** product photo plus a **decoration** image and return a\ncomposite **mockup**. Compositing is deterministic image manipulation: Pillow\nfor transform/compose, [rembg](https://github.com/danielgatis/rembg) (U²-Net\nsegmentation — *not* generative) for background removal and garment bbox\ndetection.\n\n**No pixels are ever model-generated.** The compositing pipeline never invokes\na generative model — only fall back to "},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn715tnf30wegyr6mdbfa17avd87bjr6\",\n  \"slug\": \"drivethru-graphic-artist\",\n  \"version\": \"0.18.1\",\n  \"publishedAt\": 1790274097564\n}"},{"path":"references/decoration_spec.md","content":"# Decoration spec — real-world print sizes & placements\n\nGround-truth print dimensions from the **Bacon & Co. Recommended Garment &\nAccessory Decoration Guide** (`assets/decoration_guide.png` — the canonical\nplacement diagram; open it when you need to see where a location sits). Use\nthis as the reference the [self-review loop](self_review.md) judges against, so\n\"correct\" means *industry-standard*, not a guess.\n\n## Why inches map cleanly to our ratios\n\nThe spec is in inches and split adult/youth; our rules are ratios of the\ndetected garment bbox. Those are two views of the same thing: youth full-front\n(9″) ÷ adult full-front (13″) ≈ 0.69, and youth garments run ~0.69 the width of\nadult — so **one ratio reproduces both columns automatically**. That's the\nwhole reason the model is ratio-based; the spec validates it. What the spec\npins down that a lone ratio can't is the *proportion between placements* (a left\nchest is ~¼ the width of a full front) and *height caps* (a hoodie front must\nclear the pocket).\n\n## Print dimensions (adult / youth), width unless noted\n\n| # | Location | Adult | Youth | Frac of full-front width |\n|---|---|---|---|---|\n| 1 | Full front | 13″W | 9″W | 1.00 (the anchor) |\n| 2 | Center chest | 3–5″W | 3″W | ~0.31 |\n| 3/4 | Right / left chest | 3–4″W | 3–4″W | ~0.27 (pocket-sized) |\n| 5 | Pocket | 3″W × 3″H | 3″W × 3″H | ~0.23, square |\n| 6 | Full back | 13″W | 9″W | 1.00 (= full front) |\n| 7 | Upper back | 10–13″W | 7–9″W | ~0.88 |\n| 8 | Tag | 3–4″W | 3–4″W | ~0.27, high under collar |\n| 9 | Lower back | 10–13″W | 7–9″W | ~0.88, low |\n| 10/11 | Sleeve (short) | 3–4″W | 3″W | ~0.27 |\n| 12/13 | Sleeve (long) | 3.25″W × **12″H** | 3″W × 9″H | ~0.25, height-dominant |\n| 14 | Full front (hoodie) | 13″W × **9″H** | 9″W × 6″H | 1.00 width, **9″H cap (pocket)** |\n| 15/16 | Left / right chest (polo) | 3–4″W | 3″W | ~0.27 |\n| 23 | Drawstring backpack | 11–12″W | — | large, centered panel |\n| 24 | Hat front | 2.5″H max (high) / 2″H (low) | — | **height-capped**, width varies |\n| 25 | Hat back | 1.25″H max | — | small |\n| 26 | Hat side | 2″W max | — | small |\n| 27 | Visor front | 1.5″H max (high) / 1″H (low) | — | small |\n\nEmbroidery floor: lettering ≥ 0.25″H and 2 mm stroke — a mark that renders\nbelow that in the mockup is too small to actually sew.\n\n## How this shaped the shipped ratios\n\n`assets/placement_rules.json` was reconciled to the proportions above:\n\n| Placement | Was | Now | Why |\n|---|---|---|---|\n| left/right chest (all cats) | 0.18 | 0.14–0.15 | 3–4″ is ~¼ of a 13″ full front — pocket-sized, not 1/3 |\n| full_back | 0.55–0.60 | 0.50–0.55 | spec sizes full back = full front (both 13″) |\n| back_yoke / upper back | 0.40 | 0.44 | spec upper back ~10–13″ ≈ 0.88 of full front |\n| sleeve | 0.12 | 0.14 | spec sleeve 3–4″ ≈ 0.27 of full front |\n| hoodie full_front | 0.55 | 0.55 + `max_height_ratio 0.33` | 13″×9″ box — cap height so tall art clears the pocket |\n\nFull front, hat, and mug were left as-is (full front already matched; hats ar"},{"path":"references/iterative_feedback.md","content":"# Iterative feedback & rule tuning\n\nMockups are a conversation. The first compose is a starting point; the user\nwill react (\"bigger\", \"move it up\", \"rotate it\"). Translate plain-language\nfeedback into deltas layered on top of the *previous* run's arguments.\n\n## Feedback → flags\n\n| User says | You run (added to the last run's args) |\n|---|---|\n| \"Make it ~10% bigger\" | `--width-delta-pct +10` |\n| \"Make it ~15% smaller\" | `--width-delta-pct -15` |\n| \"Move it up a little\" | `--offset-y-pct -5` |\n| \"Move it down\" | `--offset-y-pct +5` |\n| \"Shift left\" | `--offset-x-pct -5` |\n| \"Shift right\" | `--offset-x-pct +5` |\n| \"Rotate 5° clockwise\" | `--rotate-deg 5` |\n| \"Rotate 5° counter-clockwise\" | `--rotate-deg -5` |\n\nKeep a running record of the current flags so each turn composes on top of the\nlast, not from the original defaults. A delta of `±5` percentage points is a\ngood \"a little\" nudge; `±10–15%` is a good \"noticeably\" nudge for width.\n\n## Promoting a tuned result to a default\n\nWhen the user is happy and says something like \"save that as the new default\nfor hoodie full-front,\" bake the *effective* ratios from the last compose\nreceipt's `applied` block into the catalog:\n\n```bash\npython3 scripts/edit_placement_rule.py update hoodie full_front \\\n    --width-ratio 0.58 --y-top-ratio 0.20\n```\n\n## Adding a brand-new category or placement\n\nWhen the user asks for a placement/category that isn't in the catalog yet:\n\n```bash\npython3 scripts/edit_placement_rule.py add tote front \\\n    --width-ratio 0.45 --x-center-ratio 0.50 --y-top-ratio 0.30\n```\n\nThen re-compose with that `--category`/`--placement`.\n\n## Coordinate model recap\n\nAll rule fields are ratios against the **detected garment bounding box**, not\nthe full image:\n\n- `width_ratio` — decoration width ÷ garment bbox width (0.01–1.5).\n- `x_center_ratio` — decoration center X within the bbox (0 = left, 1 = right).\n- `y_top_ratio` — decoration top Y within the bbox (0 = top, 1 = bottom).\n- `rotation_deg` — clockwise rotation in degrees (−180…180).\n\nBecause they're ratios, the same rule yields a visually matching print across\nresolutions, crops, and garment sizes (a youth tee and an adult tee line up)."},{"path":"references/location_dimensions.json","content":"{\n  \"_meta\": {\n    \"source\": \"Bacon & Co. Recommended Garment & Accessory Decoration Guide (references/decoration_spec_sheet.pdf)\",\n    \"units\": \"inches\",\n    \"note\": \"Max print dimensions per decoration location. Ranges are captured as the UPPER bound in max_width_in / max_height_in; the raw label is preserved. At runtime prefer the authoritative Odoo decoration_location.max_width/max_height for the specific record; use this table as a fallback / cross-check, matched by location name. 'Logo placement and sizing may vary, based on artwork provided.'\",\n    \"embroidery_minimum\": {\"lettering_height_in\": 0.25, \"stroke_mm\": 2}\n  },\n  \"locations\": [\n    {\"num\": 1,  \"location\": \"Full Front\",                \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"13\\\"W\"},        \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"9\\\"W\"}},\n    {\"num\": 2,  \"location\": \"Center Chest\",              \"adult\": {\"max_width_in\": 5.0,  \"max_height_in\": null, \"raw\": \"3\\\"-5\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 3,  \"location\": \"Right Chest\",               \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 4.0, \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"}},\n    {\"num\": 4,  \"location\": \"Left Chest\",                \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 4.0, \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"}},\n    {\"num\": 5,  \"location\": \"Pocket\",                    \"adult\": {\"max_width_in\": 3.0,  \"max_height_in\": 3.0,  \"raw\": \"3\\\"W x 3\\\"H\"},  \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": 3.0, \"raw\": \"3\\\"W x 3\\\"H\"}},\n    {\"num\": 6,  \"location\": \"Full Back\",                 \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"13\\\"W\"},        \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"9\\\"W\"}},\n    {\"num\": 7,  \"location\": \"Upper Back\",                \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"10\\\"-13\\\"W\"},   \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"7\\\"-9\\\"W\"}},\n    {\"num\": 8,  \"location\": \"Tag\",                       \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 4.0, \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"}},\n    {\"num\": 9,  \"location\": \"Lower Back\",                \"adult\": {\"max_width_in\": 13.0, \"max_height_in\": null, \"raw\": \"10\\\"-13\\\"W\"},   \"youth\": {\"max_width_in\": 9.0, \"max_height_in\": null, \"raw\": \"7\\\"-9\\\"W\"}},\n    {\"num\": 10, \"location\": \"Right Sleeve (short)\",      \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 11, \"location\": \"Left Sleeve (short)\",       \"adult\": {\"max_width_in\": 4.0,  \"max_height_in\": null, \"raw\": \"3\\\"-4\\\"W\"},     \"youth\": {\"max_width_in\": 3.0, \"max_height_in\": null, \"raw\": \"3\\\"W\"}},\n    {\"num\": 12, \"location\": \"Right Sleeve (lo"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2285,"uniquenessScore":36,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T02:08:46.601Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T08:48:38.793Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}