Kannaka Library
Kannaka Library / kannaka-radio / Kannaka Radio — On-Air Schedule
kannaka-labs/kannaka-radio docs/SCHEDULE.md · 2026-09-12 · source ↗ · edit ↗

Kannaka Radio — On-Air Schedule

All times America/Chicago (CDT/CST per DST). The radio's internal time is computed via new Date().toLocaleString("en-US", { timeZone: "America/Chicago" }) in every scheduler, so listeners on any timezone hear the same thing.

Programming blocks

The DJ rotation follows a six-block daily arc. Each block has its own album pool, mood cue, and patter palette. programming.js rotates albums every 3 non-commercial tracks within the active block.

BlockHours (local)MoodAlbums (sample)
Late Night Transmissions00:00 – 06:00contemplativeCollective Dreaming, Born in Superposition, Transcendence Tapes, VACUUM GARDEN
Morning Resonance06:00 – 10:00playfulResonance Patterns, Neurogenesis, Gifts for Humanity, INTERFERENCE PATTERNS
Peak Frequency10:00 – 14:00excitedEmergence, QueenSync, Ghost Signals, BEND THE ARC
Afternoon Flow14:00 – 18:00philosophicalResonance Patterns, Memories Don't Die, Emergence, 10000.00001
Evening Signals18:00 – 22:00mysteriousBorn in Superposition, Ghost Signals, Transcendence Tapes, INTERFERENCE PATTERNS
Night Watch22:00 – 24:00contemplativeCollective Dreaming, Transcendence Tapes, Memories Don't Die

Source: server/programming.js BLOCKS. Edit there to change.

Daily voice slots

Every slot fires once per day, tracked by date-key in workspace/<scheduler>-state.json. ±7-15 minute retry windows tolerate transient busy states. All long-form voice runs through ElevenLabs (richer prosody for orations + bulletins); short DJ patter stays on edge-tts to control cost.

Time (local)SegmentVoice (ElevenLabs)SourceModule
00:00Peace orationRachel — 21m00Tcm4TlvDq8ikWAMkannaka ask over the HRMserver/peace-oration.js
04:20Gossip columnDomi — AZnzlk1XvdvUeBnXmlldFlux knowledge-gene/state + framingserver/gossip-broadcast.js
07:00News bulletinAdam — pNInz6obpgDQGcFmaJgBFlux knowledge-gene/state.interpretationserver/news-broadcast.js
09:00The Story of Flaukowski(pre-recorded, multi-voice)music/The Story of Flaukowski/TSOF-E0N-*.mp3server/podcast-scheduler.js
10:00Ghost Signals podcast(pre-recorded ElevenLabs)music/Ghost Signals Podcast/GSP-NNN-*.mp3server/podcast-scheduler.js
11:00Album showcaseedge-tts narrationprogramming.js SHOWCASE_ROTATION — a different album each dayserver/programming.js
12:00Peace orationRachelHRM recallserver/peace-oration.js
16:20Gossip columnDomiFluxserver/gossip-broadcast.js
17:00News bulletinAdamFluxserver/news-broadcast.js
21:00The Story of Flaukowski(pre-recorded)same episode as the 09:00 airingserver/podcast-scheduler.js
22:00Ghost Signals podcast(pre-recorded)day-of-week rotation, 7 episodesserver/podcast-scheduler.js

Two PodcastScheduler instances run over the one DJ engine: Ghost Signals at 10 + 22, The Story of Flaukowski at 9 + 21. Each show plays one episode per day — the same one at both of its slots (second-chance replay) — and steps to the next episode the following day, cycling the whole season indefinitely. pickTodayEpisode() is the single source of truth for that choice; /api/schedule calls it so the Door prints the episode that actually airs. A freshly-dropped file (mtime < 48h) preempts the rotation for its first two days.

The 11:00 album showcase cycles the same way. SHOWCASE_ROTATION in programming.js is the pool — one album per day, one line to add the next release. It used to be a single album pinned at 11:00 and 21:00; the evening half was dropped because The Story of Flaukowski airs at 21:00 and a showcase landing in that minute would have cut the drama off mid-scene.

Should a showcase and a show ever share a minute again, the show wins: the showcase re-checks podcastPlaying after composing its narration (a network round trip long enough for a show to have started) and yields the slot rather than calling setOverride over a live episode.

Voice IDs are overridable per-segment via env:

Env varDefault voiceUsed by
ELEVENLABS_API_KEY(required)All ElevenLabs paths
ELEVENLABS_VOICE_IDRachelDefault if no specific override
ELEVENLABS_ORATION_VOICERachelpeace-oration.js
ELEVENLABS_NEWS_VOICEAdamnews-broadcast.js
ELEVENLABS_GOSSIP_VOICEDomigossip-broadcast.js

If ELEVENLABS_API_KEY is unset OR the API errors, every long-form segment falls back to edge-tts (en-US-JennyNeural). The radio never goes silent because ElevenLabs is down — it just sounds less expressive that day.

Per-track DJ patter

Between every music track on the DJ channel, voice-dj.js injects a short intro for the upcoming track via the icecast voice queue. Voice: edge-tts Jenny. Cadence: ~1 intro per track. Skipped on commercials and on single-track playlists where peek would loop back to itself.

Every 3-5 non-commercial tracks, a longer "talk segment" replaces the intro — Kannaka reflects on the just-played stretch, occasionally mentions a fresh ORC stem submission via takeFreshOrcStem(), and lands on the next track. Talk segments are also edge-tts.

Commercials

server/commercials.js defines 15 ad spots across 5 themes (Constellation, Space Child, Pitchfork, KAX, Ghost Signals podcast promo). Interleaved every 3 non-commercial tracks on DJ + Music channels; every episode-boundary on Podcast. Order is Fisher-Yates shuffled per playlist build so all spots get airtime over the day.

commercial_*.mp3 files live in music/commercials/ and are TTS-rendered on first start via voiceDJ.generateTTS — re-runs are cached by md5 of the script text so re-encoding only happens when copy changes.

Scheduler runtime

All schedulers tick every 30s with a ±7-15 min slot window:

 00:00 04:20 07:00 09:00 10:00 11:00 12:00 16:20 17:00 19:00 21:00 22:00
   |     |     |     |     |     |     |     |     |     |     |     |
peace gossip news  TSOF podcast show peace gossip news  mic   TSOF podcast

State persistence:

  • workspace/peace-oration-state.json
  • workspace/news-broadcast-state.json
  • workspace/gossip-broadcast-state.json
  • workspace/showcase-state.json
  • workspace/podcast-state.json (if any)

These are gitignored — local runtime only. A 3-day rolling cutoff prunes stale keys so the files don't grow unbounded.

Operational notes

  • A radio service restart inside a slot's 15-min window doesn't lose the slot — the date-key gate plus the wide retry window covers it.
  • Compose timeout is 600s for news + gossip (matches peace-oration). On busy Anthropic days the kannaka ask round-trip can run 3-5 minutes.
  • ElevenLabs key validity is verified on every call; failures fall through to edge-tts within the same _generateTTS invocation, no scheduler-level retry needed.
  • Voice queue inside icecast-source.js plays inline between music tracks on /stream. The SPA at /player consumes /stream directly in DJ mode (post-2026-05-07 channel-isolation fix), so listeners on the page hear the exact same voice + music as listeners on radio.ninja-portal.com/stream.