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>
170 lines
8.3 KiB
Markdown
170 lines
8.3 KiB
Markdown
# 🎨 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_KEY` in the backend `.env` for clean output
|
||
- [x] ~~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/products` returns fields on the
|
||
object itself (`p["slug"]`), not under `p["attributes"]["slug"]`. The helper in
|
||
`photoroom_to_spree.py` only survives because its fallback scan happens to use
|
||
the flat form.
|
||
- **`per_page` is silently ignored** — it pages on `limit`. Sending `per_page=100`
|
||
gets you 25 rows and no error, so a scan that stops at page 1 "finds" nothing.
|
||
Meta key is `pages`, not `total_pages`.
|
||
|
||
|
||
**Keys:** `/root/.photoroom/credentials` (chmod 600) on the house box;
|
||
`PHOTOROOM_API_KEY` in the backend `.env`.
|