🦄 Unicorn.Land ▸ docs/superpowers/specs/2026-06-20-oannes-carousel-routing-design.md
updated 2026-06-20

Oannes Carousel Format + Per-Platform Routing — Design

Date: 2026-06-20 Status: Approved (brainstorming → spec) Project: Oannes Content Engine (oannes-engine/)

Goal

Add a second content format — image carousels — and route formats per platform instead of posting the same reel everywhere. Empirically: our video reels work on Facebook (1,178 plays) and YouTube (146) but are dead on Instagram (1 view across 4 posts) and TikTok (0, cold-start). A 900K-follower comparable in our exact topic space (massive_thinks) does it entirely with text-hook carousels, which Instagram rewards via saves/dwell/comments. So: keep video where it works, add carousels to rescue Instagram, and make the format→platform mapping configurable.

Non-negotiable constraint

Additive and gated. The reel path and the running daily machine keep working exactly as today. The carousel path is new code; if carousel generation fails, the reel still publishes (and vice versa) — one format’s failure never blocks the other.

Locked decisions

  1. Routing = format-per-platform (option A). A configurable map: { facebook: "reel", youtube: "reel", tiktok: "reel", instagram: "carousel" }. Configurable so adding tiktok: "carousel" later is a one-line change. Reel → FB/YT/TikTok; carousel → Instagram.
  2. One claim → both artifacts per drop. Each daily drop selects one claim (existing vibe/art logic) and renders it both as a reel (as today) and as a carousel. Same claim = cross-platform coherence; carousels are cheap (stills only, no ElevenLabs/ ffmpeg). Each format publishes only to its mapped platforms.
  3. Carousel = ~6 slides, 1080×1350 (Instagram’s 4:5 portrait sweet spot).
  4. Discord preview = a contact-sheet montage of all slides in one image, same ✅/❌ approval gate as reels.
  5. Attribution is the differentiator. We borrow massive_thinks‘s format mechanics (bold hook, curiosity gap, swipe payoff) but keep the mandatory “X says Y” attribution — the credibility/IP shield that distinguishes us from ragebait.

Generated by a new carousel scriptwriter (claim → slides), ~6 slides:

Slide Purpose Content
1 Hook Bold curiosity-gap line, keywords highlighted, over a striking AI image. Forces the swipe.
2–4 Reveal The claim built one beat per slide.
5 Attribution payoff “{Name} · {role}” — who actually claims this. The honesty anchor.
6 CTA Soft “Follow @oannescode for more.”

Architecture

New module work, mirroring the existing reel pipeline’s shape:

File Responsibility New/Modify
src/script/carouselscript.ts claim + vibe → CarouselScript (slides + IG caption + hashtags) via claude --print Create
src/render/Carousel.tsx restyle CarouselSlide to match reel brand chrome; accept a per-slide bg image Modify
src/render/carousel.ts renderCarouselSlides(script, bgImages, outDir) → string[] (one PNG/slide) Create
src/media/montage.ts buildMontage(slidePaths, outPath) — contact-sheet for Discord preview (ffmpeg tile) Create
src/publish/publishCarousel.ts upload N slide images → publish IG carousel via Blotato → poll to URL Create
src/publish/routing.ts FORMAT_BY_PLATFORM map + platformsForFormat(format) Create
src/auto/daily.ts also build the carousel + montage; queue both artifacts Modify
src/auto/state.ts PendingItem gains carousel?: { slidePaths, caption, hashtags } + per-format results Modify
src/auto/approvals.ts on ✅, publish reel→reel-platforms AND carousel→carousel-platforms Modify
src/metrics/* add format (“reel” “carousel”) column + dimension to snapshots/analysis

Data flow (per drop)

claim → reel script  → reel.mp4    → (queue)
      → carousel script → slide PNGs → montage.png → (queue)
Discord: post montage + reel preview, ✅/❌
✅ → publish reel  → FB, YT, TikTok   (via routing map)
   → publish carousel → Instagram     (via routing map)
   → record results tagged format·platform·vibe·art·hook·topic

publishImagePost already sends content.mediaUrls: [url1, url2, …]. Before depending on it, verify Blotato renders a multi-image array as a real Instagram carousel (not just the first image) — test via a dry-run and one unlisted/throwaway post. If Blotato can’t do native IG carousels, fall back options (in order): (a) Blotato’s documented carousel/gallery field if one exists; (b) post as a single tall stitched image. This check gates the publish design and is task 1 of implementation.

Learning loop

oannes_post_metrics gains a format column. Stickiness is unchanged (per-view engagement). Analysis groups by (platform, format, …) so the weights file can express “carousel beats reel on Instagram.” Generation bias is unchanged for now — we just want the format × platform data; acting on it (e.g. auto-routing by measured format performance) is explicitly future work.

Error handling

Testing (TDD, tsc-gated via npm test)

Build order (commits)

  1. Feasibility + routing — verify Blotato IG carousel; routing.ts + map.
  2. Carousel render — scriptwriter + restyled slides + renderCarouselSlides + montage.
  3. PublishpublishCarousel.ts (upload-many → IG carousel → poll/reconcile).
  4. Wire into daily machine — daily builds both; approvals publishes both by route; Discord montage preview.
  5. Learning loopformat dimension end to end.

Out of scope (v1)