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
- Routing = format-per-platform (option A). A configurable map:
{ facebook: "reel", youtube: "reel", tiktok: "reel", instagram: "carousel" }. Configurable so addingtiktok: "carousel"later is a one-line change. Reel → FB/YT/TikTok; carousel → Instagram. - 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.
- Carousel = ~6 slides, 1080×1350 (Instagram’s 4:5 portrait sweet spot).
- Discord preview = a contact-sheet montage of all slides in one image, same ✅/❌ approval gate as reels.
- 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.
Carousel slide structure
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
Blotato IG carousel — feasibility check (do first)
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
- Carousel generation throws → log, skip the carousel, still queue/publish the reel (and the Discord failure alert fires). Reel generation throws → reel path’s existing behavior; carousel can still proceed.
- Blotato IG-carousel publish fails/times out → same reconcile-by-mediaUrl pass the reels use; persist the error reason.
- No Replicate key → slide backgrounds fall back to the gradient (as reels already do).
Testing (TDD, tsc-gated via npm test)
carouselscript.ts: prompt builder + JSON parse/validation (zod), attribution required.routing.ts: map → platforms-for-format, pure.publishCarousel.ts: upload-many + publish shape with mockedfetch.montage.ts: tile-command builder (pure; no live ffmpeg in CI).metrics:formatdimension flows through aggregate; existing tests updated.- Follow existing vitest patterns (injected
fetch, tmpdir state).
Build order (commits)
- Feasibility + routing — verify Blotato IG carousel;
routing.ts+ map. - Carousel render — scriptwriter + restyled slides +
renderCarouselSlides+ montage. - Publish —
publishCarousel.ts(upload-many → IG carousel → poll/reconcile). - Wire into daily machine — daily builds both; approvals publishes both by route; Discord montage preview.
- Learning loop —
formatdimension end to end.
Out of scope (v1)
- Auto-routing by measured format performance (collect the data first).
- TikTok photo-mode carousels (one-line config add once IG carousels are proven).
- Per-platform different captions for the carousel (reuse one IG-tuned caption).