Skip to content

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

CapabilityStatus
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)

KeyDefaultEffect
ai_mockup_enabledtrueMaster switch; when false, no planning or advancement runs
ai_mockup_cron_day_of_week1,4Comma-separated 0–6 (Sun–Sat) days to plan a new run
ai_mockup_cron_hour_utc2Hour (0–23 UTC) on the scheduled day to plan
ai_mockup_target_count6Photos to fetch per run
ai_mockup_printful_catalog_product_id1Printful catalog product (1 = Enhanced Matte Poster)
ai_mockup_printful_catalog_variant_id19527Printful variant (A1 poster size)
ai_mockup_autopromotetrueWhen 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)

TablePurpose
ai_mockup_runsOne row per planned collection; state machine across 7 stages
ai_mockup_printful_tasksOne 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 = NULL
  • designs.source = 'unsplash', full photographer attribution persisted (Unsplash API guideline compliance)
  • products.pod_creator_id = NULL
  • designs.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

FailureBehavior
LLM planning unavailableEvergreen hardcoded plan (rotates through 4 themes); run still proceeds
Unsplash rate limit / no results for a queryThat query is skipped; partial runs continue; pipeline fails only if 0 photos fetched
Printful task failureTask marked failed; run continues with the rest; advance to publishing once all tasks terminal
Printful auth missingRun marked failed with descriptive message
Publish crashRun marked failed; admin can Retry — accumulated state is wiped, plan preserved
Storage GC sweepAI 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 draw textOverlays[i] onto each Unsplash photo before Printful rendering. The downstream stages are already wired to read composite_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 × N Printful 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-cron shows [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 to planning.

Synergy Books · SYS-BOOK Series