🦄 Unicorn.Land ▸ docs/superpowers/specs/2026-06-27-oannes-per-post-variety-design.md
updated 2026-06-27

Oannes Engine — Per-Post Variety (rotate voices + narration structures)

Date: 2026-06-27 Status: Approved (design) Roadmap item: #2 from the 2026-06-27 deep-research strategy Related: docs/research/2026-06-27-faceless-brand-growth-research.md, docs/superpowers/specs/2026-06-16-oannes-learning-loop-design.md

Problem

The deep-research strategy (2026-06-27) confirmed that YouTube’s July-2025 “inauthentic content” policy targets templated / reused-narration content. The Oannes pipeline’s signature — one ElevenLabs voice (Ash), one narration template, twice a day — is exactly that pattern. Per-post variety is roadmap item #2 (after DM-shareable hooks, which shipped in 7ce52ee).

Account-type gate (roadmap #3) was verified the same session: TikTok @oannes_code is a Personal account and Instagram @oannescode is a Creator account — neither is Business, so the trending-audio lockout does not apply. (Informational; not part of this build.)

Goal

Rotate narrator voice and narration structure per post so no two consecutive drops share the same voice or structure, and wire both into the existing learning loop as new dimensions (alongside vibe / artKey / format) so the engine can eventually learn which voice/structure performs best per platform.

Decisions locked with Joshua: - Voice pool: on-brand male narrator voices (channel stays coherent; not a wide gender/accent mix). - Learnable now: full learnable dimensions, not just rotation. Voice + structure feed the money-weighted epsilon-greedy bias like vibe/artKey.

Non-goals (YAGNI / v1 scope boundaries)

Design

1. Voice pool — src/media/voices.ts (new)

On-brand male narrator voices, all narrator-grade for awe/mystery:

key voice ElevenLabs id
ash Ash — calm, soothing, magnetic (current default) VU16byTywsWv5JpI8rbc
brian Brian — deep, resonant, comforting nPczCjzI2devNBz1zQrb
george George — warm, captivating storyteller (British) JBFqnCBsd6RMkjVDRZzb
bill Bill — wise, mature, gravitas pqHfZKP75CvOlQylNhV4

Exports VOICE_POOL: { key: string; voiceId: string; label: string }[] and helpers to look up a voiceId by key. Voice applies to reels only.

2. Structure archetypes — src/script/structures.ts (new)

Each archetype is a short directive injected into buildReelPrompt. All preserve the existing HARD RULES (attribution, no fabrication, no criminal accusations) and the forwardable-hook goal; they govern the shape of the narration body, not the rules.

key directive (summary)
cold_open Drop straight into the strangest image/detail, then reveal who said it and what it means.
question_spiral Open on a provocative question, escalate through 2-3 deeper questions, land on the claim as the unsettling answer.
witness Second-person immersion (“Picture this…”, “Imagine you’re…”) placing the viewer in the scene, then attribute.
contrarian State the consensus / what everyone assumes, then pivot hard to the claimant’s challenge.
escalation Start small and plausible, ratchet through increasingly wild implications to a mind-bending payoff.
confession Frame as a startling admission/revelation the claimant made that most experts won’t say out loud.

Exports STRUCTURES: { key: string; directive: string }[] and a structureDirective(key) helper.

3. Rotation + learning — src/auto/rotate.ts, src/metrics/apply.ts

Mirrors the existing artKey path exactly.

4. Script + generation — src/script/reelscript.ts, src/auto/daily.ts

5. Metrics plumbing — src/metrics/store.ts, src/metrics/collect.ts, src/metrics/analyze.ts

Applies the lesson from the format-column bug (the format index in CREATE_TABLE_SQL ran before its ALTER, silently killing collection 06-20→06-24):

6. State — src/auto/state.ts

PendingItem gains optional voice?: string (the voice key) and structure?: string. Optional so older queued items load without them.

7. Discord preview — src/auto/daily.ts

The preview content line gains 🎙️ <voice label> · 🧬 <structure key> for at-a-glance confirmation and debugging.

Data flow (unchanged shape, two new tags)

planTodayLearned → {vibe, art, voiceKey, voiceId, structure}
  → daily.ts: writeReelScript(structure) + TTS(voiceId)
  → PendingItem{voice, structure, …}
  → publish → results
  → collect.ts: MetricSnapshot{voice, structure, …}  (carousel → "")
  → store: oannes_post_metrics(voice, structure, …)
  → analyze: PlatformWeights{voice{}, structure{}, …}
  → apply/rotate: blendedDimensionScores("voice"|"structure") → next pick

Testing

New/extended unit tests (tsc-gated, existing vitest suite): - rotate: no-consecutive-repeat for voice and structure; planTodayLearned returns the new fields; null-weights path is unbiased rotation. - apply: blendedDimensionScores over "voice"/"structure" blends per-platform/confidence correctly. - store: buildInsert emits the right column count/order including voice/structure. - analyze: aggregate computes voice/structure means and skips empty keys; schema parses a legacy weights file (no voice/structure) via defaults. - reelscript: prompt includes the structure directive when a structure is passed, omits it otherwise. - collect: snapshot carries voice/structure for reel results and "" for carousel results.

Risks & mitigations