Phase 1 of an automated cold-start solution. An LLM picks a theme based on the season and upcoming holidays, fetches Unsplash photos that match, submits Printful mockup tasks for them, and publishes them as catalog products attributed to the system account (@audlis). This keeps the storefront populated with fresh, on-trend designs without manual content production.
TL;DR
| Capability | Status |
|---|---|
| LLM picks theme from date + holiday knowledge | ✅ |
| Unsplash photo fetch (server-side, with attribution) | ✅ |
| Text overlay composition on photos | ⏳ Phase 2 (passthrough in Phase 1) |
| Printful mockup rendering (auto poll) | ✅ |
Auto-publish to /shop products + /discover designs | ✅ |
Federates via @audlis service actor | ✅ (via existing promote hook) |
| Fully cron-driven; admin can also trigger manually | ✅ |
| Configurable schedule / target count / Printful product / autopromote | ✅ |
Pipeline overview
[cron: scheduled day/hour] ──▶ planNextCollection
LLM gets current date + upcoming holidays + last 4 weeks' themes
Returns: { theme, rationale, unsplashQueries[], textOverlays[], palette }
INSERT ai_mockup_runs (stage='planning')
│
▼
[cron: every minute, advance one active run]
stage='planning' → fetchCollectionAssets
For each Unsplash query: search → pick first → fetch bytes → R2 → designs row
stage → 'compositing'
│
▼
stage='compositing' → compositeRunDesigns
Phase 1: passthrough (no overlay). Phase 2 will plug in @cf-wasm/photon
stage → 'mockup_pending'
│
▼
stage='mockup_pending' → submitPrintfulMockupsForRun (one-shot)
For each design: createPrintfulMockupTask → INSERT ai_mockup_printful_tasks
│
▼
[cron: every minute, separate tick]
pollPendingPrintfulTasks (batch=5)
For each pending task: getPrintfulMockupTask
completed → fetch mockup bytes → R2 → INSERT personalized_items
failed → record error, don't block run
When all run's tasks terminal → stage → 'publishing'
│
▼
stage='publishing' → publishCompletedRun
For each personalized_item: INSERT products (visibility=active, autopromote)
+ UPDATE designs SET visibility='public', license='free'
(Federation broadcast fires via the existing promote hook.)
stage → 'completed'Configuration (app_config keys)
| Key | Default | Effect |
|---|---|---|
ai_mockup_enabled | true | Master switch; when false, no planning or advancement runs |
ai_mockup_cron_day_of_week | 1,4 | Comma-separated 0–6 (Sun–Sat) days to plan a new run |
ai_mockup_cron_hour_utc | 2 | Hour (0–23 UTC) on the scheduled day to plan |
ai_mockup_target_count | 6 | Photos to fetch per run |
ai_mockup_printful_catalog_product_id | 1 | Printful catalog product (1 = Enhanced Matte Poster) |
ai_mockup_printful_catalog_variant_id | 19527 | Printful variant (A1 poster size) |
ai_mockup_autopromote | true | When false, runs complete without creating product rows; admin curates via MockupCurationManager |
ai_mockup_attribution_user_id | (empty) | Empty = system attribution (NULL user_id, free license) |
These are editable via POST /api/admin/settings (the same endpoint SiteSettingsManager uses). The values are read fresh by the cron worker on every tick, so changes take effect within 60s.
Admin UI
/admin?view=ai-mockups shows:
- Current config (read-only display; edit in SiteSettings)
- Recent runs (theme, stage badge, progress, error if any)
- "+ Plan new run" button — manually triggers planning outside the schedule
- "Retry" button on failed runs — resets stage to
planning
/admin?view=mockups (the existing MockupCurationManager) shows AI-generated mockups alongside user submissions. With autopromote=false, admin can selectively promote AI mockups from here.
Data model (migration 027)
| Table | Purpose |
|---|---|
ai_mockup_runs | One row per planned collection; state machine across 7 stages |
ai_mockup_printful_tasks | One row per Printful mockup task; polled by cron |
Stage values: planning → fetching → compositing → mockup_pending → publishing → completed | failed.
Attribution & licensing
AI mockups have no human creator:
designs.user_id = NULLdesigns.source = 'unsplash', full photographer attribution persisted (Unsplash API guideline compliance)products.pod_creator_id = NULLdesigns.license_type = 'free'(no royalty split)
The federation broadcast (if @audlis is followed on the Fediverse) attributes the Create activity to the service actor — Phase 1/2 federation paths already handle NULL-user_id designs via resolveAttributionActorId.
Failure modes & recovery
| Failure | Behavior |
|---|---|
| LLM planning unavailable | Evergreen hardcoded plan (rotates through 4 themes); run still proceeds |
| Unsplash rate limit / no results for a query | That query is skipped; partial runs continue; pipeline fails only if 0 photos fetched |
| Printful task failure | Task marked failed; run continues with the rest; advance to publishing once all tasks terminal |
| Printful auth missing | Run marked failed with descriptive message |
| Publish crash | Run marked failed; admin can Retry — accumulated state is wiped, plan preserved |
| Storage GC sweep | AI designs are referenced by personalized_items rows, so the GC's "skip referenced" branch protects them |
Phase 2 roadmap (not yet shipped)
- Text overlay composition — replace
compositeRunDesigns's passthrough with a real compositor. Plan: integrate@cf-wasm/photon(or an equivalent Workers-compatible wasm image lib) to drawtextOverlays[i]onto each Unsplash photo before Printful rendering. The downstream stages are already wired to readcomposite_design_ids_json, so no other changes needed. - Multi-product — fan one design across multiple Printful SKUs (poster + tote + mug) instead of one. Requires
target_count × NPrintful tasks. - Trend enrichment — optionally feed Google Trends / Pinterest Trends data into the LLM prompt for more current themes (today the LLM uses its training-data knowledge of seasonality only).
Debugging
- Live cron logs:
npx wrangler tail audlis-cronshows[ai-mockup]lines for every stage transition and Printful poll. - List runs:
GET /api/admin/ai-mockup/runs(admin session required) — returns the last 20 runs with full state. - Manual trigger:
POST /api/admin/ai-mockup/runs(admin session, empty body) — kicks off planning immediately. - Retry:
POST /api/admin/ai-mockup/runs/{id}/retry— resets a failed run back toplanning.
