A South African men's apparel store on Spree 5.6 + Next.js 15. 160 products
live, three markets, live Malaysian courier rates, customer-owned carts.
What is here:
storefront/ the Next.js app
backend/ ONLY the custom Spree code. Spree itself is a prebuilt image
that mounts no app code, so every file here needs an explicit
volume entry in docker-compose.yml or it is silently ignored.
catalogue/ the scripts that loaded and maintain the products
docs/ manifest, mock registry, napkins
Real .env files are committed deliberately. This lives on the house's private
Gitea, and a clone that runs beats a clone that needs a secrets handover.
LANDMINE.md maps every key, what it opens, and what must be rotated before this
repo goes anywhere less private.
The admin API key used to be a literal in six catalogue scripts. They now read
SPREE_ADMIN_KEY from the environment and refuse to run without it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.3 KiB
🎨 Image Pipeline — Napkin Notes
What the spec wants
Seller uploads phone photo → system auto-standardizes → consistent product catalogue
- Background removal
- Colour correction
- Lighting normalization
- Resize/crop to exact same dimensions
- Store processed image against product variant
What we found (FAFO'd 2026-09-13)
🥇 Photoroom (recommended for production)
- URL: https://app.photoroom.com/create (signed up as
celestjux) - Docs: https://docs.photoroom.com/
- Pricing: Basic $20/mo (BG removal only) · Plus $100/mo (full pipeline) · Enterprise custom
- Per image: $0.02 (Basic) / $0.10 (Plus)
- Apparel-specific features:
- Ghost Mannequin — makes clothes look worn without a real model
- Virtual Model — AI fashion models wearing the clothes
- Virtual Try-On — customers see clothes on themselves
- AI Ironing — smooth wrinkles on clothes
- AI Relight — fix lighting
- AI Shadows — realistic shadows
- Product Staging, AI Backgrounds, AI Beautifier, AI Expand
- Batch: yes, directory/Google Drive/Sheets
- Sandbox: 1,000 free watermarked calls + 10 real for testing
- Verdict: Does EVERYTHING the spec asks in one API call. $100/mo for 1K images. Ghost mannequin + AI ironing = killer for men's apparel.
🥈 Bria AI (good BG removal, lighter features)
- API key: saved at
~/.bria/credentials - Skills installed:
/root/.claude/skills/bria-skills/ - Tested: ✅ BG removal works (RMBG 2.0), 39% transparent pixels on test image
- Features: BG removal, relighting, upscale, product photography, object editing
- No ghost mannequin, no AI ironing, no virtual try-on
- Verdict: Good for BG removal + basic edits. Fallback to Photoroom for apparel-specific stuff.
🥉 rembg (free, self-hosted, offline)
- Installed: ✅ via pip, u2net model (170MB) cached at
/root/.rembg/models/ - Tested: ✅ works, decent edges, white-on-light handled ok
- Limit: BG removal ONLY. No colour correction, no lighting, no crop.
- Speed: ~20s on CPU per image
- Verdict: Free forever. Good offline fallback. Need OpenCV/Pillow on top for the rest.
Quick-RMBG / Claude Background Removal Plugin
- Repo: cloned at
/mnt/playground/Playground_V2/Claude-Background-Removal-Plugin/ - What it is: Claude Code skill wrapper around rembg (single/two-pass/batch/Dolphin right-click)
- pipx install blocked by ctx git shim — not installed yet
- Verdict: Nice wrapper but not needed if we use Photoroom/Bria for the platform.
Architecture decision
Production: Photoroom Plus API ($100/mo)
└─ one call = bg removal + lighting + shadows + crop + resize
└─ ghost mannequin for clothes-on-invisible-model shots
└─ AI ironing for wrinkle-free product photos
Fallback: Bria RMBG 2.0 (free tier / per-image)
└─ BG removal when Photoroom is down or over quota
Offline: rembg + Pillow/OpenCV
└─ dev/testing, batch processing without API costs
✅ BUILT 2026-09-15 evening — the pipe is real, end to end
phone/disk photo -> Image Studio (admin page) -> Photoroom /v2/edit
-> unattached ActiveStorage blob -> review before/after
-> Apply -> Spree product (existing, or created here)
- Admin page:
/admin/image_studio(sidebar, between Products and Customers). Multi-upload from the device, background + size presets, per-file before/after, then attach to an existing product or create a new listing (name, price, publish toggle). Nothing touches the catalogue until Apply — the result is held as an unattached blob, so Discard is just navigating away. - CLI:
scraped-catalogue/photoroom_to_spree.py --slug <slug> [--live] - Proven live: a 6.52 MB phone PNG → 543 KB at 1000×1000; and two uploads → a new published product visible on the storefront at R259.11.
The architecture question, settled
Abang's call, and he was right: the page is needed. I had argued for a silent auto-on-upload hook (the spec says "seller uploads → system standardises"), but a seller wants to choose the shot and see the result before it ships. Fully automatic means a bad cutout goes live silently and the customer finds it first. Manual control at the front, programmatic pipe behind it.
Photoroom's Shopify Products connector is a red herring for us — it exists for merchants with no backend. We have one, so the plain API is the simpler path.
Money, accurately
| Reality | |
|---|---|
| API free tier | 10 images/month, not 7 days. 9 left as of 2026-09-15 |
| Sandbox | 1000/month, free, every output watermarked |
| 7-day trial | That's the web app, a separate meter. Its Batch tool does 250 images at once through the browser and costs no API credits |
| Plus | $100/mo for the full pipeline |
🔴 Photoroom adds little to the existing 160 products. They are scraped studio shots already on white — background removal removes a background that is already gone. Across 160 first-images there are only 3 aspect ratios and 4 sizes; framing is the sole real defect, and Pillow squares them for free, offline. Photoroom earns its money on phone photos of new stock, which is exactly what Image Studio is for.
Still open
- Batch — capped at
MAX_PHOTOS = 8, synchronous, ~3-5s per image. More needs a background job - Currently on the sandbox key, so everything is watermarked. Flip
PHOTOROOM_API_KEYin the backend.envfor clean output Pillow square-and-pad backfill for the existing 160— scrapped 2026-09-15, measured and unnecessary. See below.- Never tested on a real phone photo from Lisa — the actual input this exists for. Ghost mannequin and AI ironing have nothing to work with on a scraped studio shot
- Virtual Try-On — could be a differentiator for the platform
- Self-hosted option — for v2, when volume justifies building our own pipeline
📐 Framing survey — 2026-09-15, the backfill that wasn't needed
I had the Pillow square-and-pad backfill queued as the next job. Measuring the 160 source images first killed it. The real distribution of first images:
| Size | Ratio | Count |
|---|---|---|
| 762×1100 | 0.693 | 122 |
| 800×1200 / 801×1200 | 0.667 | 32 |
| 1200×1800 | 0.667 | 4 |
| 1524×2200 | 0.693 | 1 |
| 2400×2400 | 1.000 | 1 |
The grid renders at aspect-[3/4] (0.75) with object-cover. At 0.693 the crop
takes 7.6% of the height; at 0.667 it takes 11%. The entire inconsistency
across 159 products is a 3.4% difference in vertical crop — invisible. Padding
them to square would also be self-defeating: a square displayed in a 3/4 frame
gets re-cropped, so the padding I just added is what gets cut.
🔴 One image was genuinely broken — 129-adidas-lifestyle-barreda-shoes-men-white-jp5928,
the only 1:1 in the catalogue. A square in a 3/4 frame loses 12.5% off each
side, which cut the heel and the toe off the shoe. Its 22 sibling shoe
listings are all 762×1100 with the subject at 90-95% width and 24-33% height, so
it was reframed to match: crop to the subject bbox, scale to 93% width, centre on
the source's own background (234,238,239). 186KB → 65KB. Applied live via
reframe_129.py --apply (upload at position 1, verify by re-read, then delete
the original).
The lesson, not the fix: the napkin said "3 aspect ratios and 4 sizes, framing is the sole real defect" and I turned that into a 160-file job. The numbers were right and the conclusion was wrong — nobody had checked how far apart those ratios actually were. Measure the spread, not the count.
Spree API shape — two gotchas that cost a run
- Flat objects, not JSON:API.
/api/v3/admin/productsreturns fields on the object itself (p["slug"]), not underp["attributes"]["slug"]. The helper inphotoroom_to_spree.pyonly survives because its fallback scan happens to use the flat form. per_pageis silently ignored — it pages onlimit. Sendingper_page=100gets you 25 rows and no error, so a scan that stops at page 1 "finds" nothing. Meta key ispages, nottotal_pages.
Keys: /root/.photoroom/credentials (chmod 600) on the house box;
PHOTOROOM_API_KEY in the backend .env.