setup-guides

Season Media Pipeline — Setup Guide

The Season Media Pipeline produces a movie poster, character portraits, podcast audio, and a YouTube video for each completed Story Season.

Stages:

  1. Visual assets — DALL-E movie poster + per-cast portraits
  2. Voice casting — auto-assigns ElevenLabs voices to each persona
  3. Script generator — narrator overview + per-entry segments + transitions
  4. Audio render — ElevenLabs TTS + ffmpeg compilation w/ chapter markers
  5. Video render — Ken Burns slideshow on portraits + lower-thirds + outro
  6. Distribution — YouTube Data API upload + podcast RSS feed

One-time setup (after first deploy)

1. Apply migration 094_season_media_assets.sql

Open the Supabase SQL Editor and run the contents of supabase/migrations/094_season_media_assets.sql.

This adds:

  • story_seasons.poster_url, narrator_voice_id, assets_generated_at, auto_publish
  • story_cast.portrait_url, voice_id, voice_name
  • season_audio_segments table (ordered script per season)
  • season_publishings table (YouTube + podcast distribution status)

2. Create the season-assets storage bucket

In the Supabase Dashboard → Storage:

  1. Click New bucket
  2. Name: season-assets
  3. Public: Yes (poster + portraits are shown on public season pages)
  4. Click Create bucket

Then under Policies, add:

-- Anyone can read (public bucket already implies this, but explicit is safer)
CREATE POLICY "Public read season assets"
ON storage.objects FOR SELECT
USING (bucket_id = 'season-assets');

-- Only the backend service role writes to this bucket (no client-side upload)
CREATE POLICY "Service role manages season assets"
ON storage.objects FOR ALL
TO service_role
USING (bucket_id = 'season-assets');

3. Environment variables

Required by stage:

StageEnv varNotes
1. VisualsOPENAI_API_KEYAlready set
3. ScriptOPENAI_API_KEYAlready set
4. AudioELEVENLABS_API_KEYCreator plan: $22/mo, ~100 min/mo (~1 season). Pro: $99/mo, ~500 min/mo (~5 seasons).
4.5. MusicREPLICATE_API_TOKENMusicGen-stereo-large via replicate.com — ~$0.50/season. Pay-as-you-go billing, no monthly minimum.
Distribution → YouTubeYOUTUBE_OAUTH_CLIENT_ID, YOUTUBE_OAUTH_CLIENT_SECRET, YOUTUBE_OAUTH_REFRESH_TOKENSee section below

Stages 2, 5, and the podcast RSS feed don't need any extra keys.

4. YouTube OAuth setup (only needed for the YouTube upload button)

  1. Google Cloud Console: create a new project (or pick an existing one) → APIs & Services → enable YouTube Data API v3.
  2. OAuth consent screen: choose External, fill in app name + your email, add scope https://www.googleapis.com/auth/youtube.upload. Add yourself as a test user.
  3. CredentialsCreate CredentialsOAuth client ID → Application type "Web application". Add https://developers.google.com/oauthplayground as an authorized redirect URI. Copy the Client ID and Client Secret.
  4. OAuth Playground (developers.google.com/oauthplayground) → gear icon (top right) → check "Use your own OAuth credentials" and paste your client ID + secret. In the left sidebar paste this scope: https://www.googleapis.com/auth/youtube.upload. Click Authorize APIs, complete the Google login, then click Exchange authorization code for tokens. Copy the Refresh token.
  5. Set env vars on Railway (or .env locally):
    YOUTUBE_OAUTH_CLIENT_ID=...
    YOUTUBE_OAUTH_CLIENT_SECRET=...
    YOUTUBE_OAUTH_REFRESH_TOKEN=...
    
  6. The first upload publishes as unlisted by default — you can flip it to public on YouTube once you've reviewed it.

5. Submitting the podcast feed

Once the backend is deployed, the public RSS feed is at:

https://<your-backend-host>/api/podcast/rss

Apple Podcasts: podcastsconnect.apple.com → "+" → New Show → paste the RSS URL.

Spotify for Podcasters: podcasters.spotify.com → Add a podcast → Existing podcast → paste the RSS URL.

The feed updates automatically as you publish new seasons (clicking the 🎧 Podcast button on each season). Cache TTL is 5 minutes.

Per-season usage

In /dashboard/admin/seasons, each season row has these stage-trigger buttons:

ButtonStageCostTime
🎨 Visuals1: poster + portraits~$0.88~60s
🎤 Cast Voices2: assign ElevenLabs voices$0<1s
✏️ Edit Voicesmanual override modal$0manual
📝 Script3: build narrator + cast script<$0.01~5s
🎙️ Render Audio4: ElevenLabs TTS + ffmpeg concat~$25 (75 min)~5+ min
🎵 Score Music4.5: mood scoring + MusicGen cluster cues + per-character leitmotifs + sidechain mix~$0.80~7-12 min
🎼 Themespreview each character's leitmotif (visible after Score Music runs)$0instant
🎬 Render Video5: Ken Burns slideshow MP4$0 (CPU only)~5-15 min
🎧 Podcastdistribution: publish/unpublish$0<1s
▶️ YouTubedistribution: unlisted upload$0minutes (depends on size)

Each row also has an Auto-publish on complete checkbox — when set, the full pipeline runs automatically when you click ▶ Next Episode on the final episode (or any other action that flips status to completed).

Stages can be re-run independently. The pipeline is idempotent — re-running visuals when a poster already exists will prompt before regenerating.

Cost expectations

ItemCost
Movie poster (DALL-E 3 HD, 1024×1792)$0.08
Each character portrait (DALL-E 3 HD, 1024×1024)$0.08
Typical 10-cast season visuals~$0.88
ElevenLabs Creator plan (Stage 4)$22/mo, ~100 min audio/mo
ElevenLabs Pro plan (Stage 4)$99/mo, ~500 min audio/mo

Pre-launch checklist (before submitting to Apple/Spotify)

Before clicking "Submit" in any podcast directory, verify:

  • At least one episode published — Apple rejects empty feeds. Render audio for one season and click 🎧 Podcast to publish it.
  • Channel cover art uploaded — 3000×3000 PNG/JPG, sRGB, max 500KB. Upload to season-assets/podcast/cover-3000.png and update PODCAST_FALLBACK_IMAGE in apps/backend/src/services/season-distribution.ts to the public URL. (See P1 task: "Podcast: design 3000×3000 cover art".)
  • Show description set — currently PODCAST_DESCRIPTION defaults to a generic blurb. Update to your final 200-300 char show pitch.
  • Category chosenPODCAST_CATEGORY defaults to Fiction → Drama. Apple's full category list: help.apple.com/itc/podcasts_connect/#/itcb54353390.
  • Owner email valid — Apple emails the <itunes:owner><itunes:email> address to confirm ownership. Currently set to sandonjurowski@gmail.com.
  • Feed validates — paste the RSS URL into castfeedvalidator.com or podba.se/validate and resolve any errors before submitting.
  • (Recommended) Trailer episode — Apple favors shows with a trailer. Render a 1-2 min intro and add it before submission.

Submission walkthrough

Once the checklist is complete, the RSS feed URL is:

https://hivejournalbackend-production.up.railway.app/api/podcast/rss

(Replace with your actual Railway hostname if different — check railway status.)

Apple Podcasts (1-3 day approval)

  1. Go to podcastsconnect.apple.com, sign in with your Apple ID.
  2. Click +New ShowAdd a show with an RSS feed.
  3. Paste the RSS URL, click Add. Apple validates the feed.
  4. Apple sends a verification email to the <itunes:owner><itunes:email> address. Click the link to confirm ownership.
  5. Choose category (auto-filled from your feed), language, content rights.
  6. Click Publish. Approval takes 1-3 business days; you'll be emailed when it's live.

Spotify for Podcasters (hours)

  1. Go to podcasters.spotify.com.
  2. Add a podcastExisting podcast (RSS) → paste the RSS URL.
  3. Spotify auto-validates and pulls the metadata. Approval is usually under 24 hours.
  4. Spotify auto-distributes to Amazon Music since they share infrastructure — no separate submission needed there.

Other directories (extends reach, not required)

DirectoryURLApprovalNotes
Pocket Castspocketcasts.com/submit<24 hrsFree, popular among podcast power users
Overcast(auto)Indexes from Apple Podcasts automatically once approved there
Castboxcastbox.fm/dashboard1-2 daysHeavy in Asia + Android markets
iHeartRadioiheart.com/podcast/submit1-2 weeksSlow but high-traffic
Stitcher(deprecated)Shut down 2023 — listeners moved to SiriusXM Podcasts
Google Podcasts(deprecated)Shut down 2024 — replaced by YouTube Music

Apple + Spotify cover ~80% of podcast listeners. The others are diminishing returns.

Failure modes

  • DALL-E refusal — content policy blocks dark/violent prompts. The service logs and stores null for that asset; re-run the generate-visuals endpoint to retry with adjusted prompts.
  • Bucket missing — uploads fail silently with a logged error and portrait_url/poster_url stay null. Check the storage bucket exists (Step 2 above).
  • OpenAI rate limit — portraits are batched at concurrency 3 to stay under per-minute limits. If you hit limits anyway, wait a minute and re-run.
  • ElevenLabs 401 — API key wrong, revoked, or wrong permissions. The key only needs Text to Speech: Access enabled.
  • ElevenLabs 429 — character quota exhausted for the month. Upgrade plan or wait for the next billing cycle. Plans: elevenlabs.io/pricing.
  • ffmpeg concat fails with "non-monotonous DTS" — happens when segment MP3s have inconsistent encoding. Currently the pipeline uses -c copy; if this becomes a problem, switch to -c:a libmp3lame -b:a 128k to re-encode (slower but bulletproof).
  • YouTube upload "quotaExceeded" — YouTube Data API has a default quota of 10,000 units/day; an upload costs 1,600 units. So ~6 uploads per day max on the free tier. Apply for higher quota at console.cloud.google.com/iam-admin/quotas if you scale.
  • Apple feed rejection: "missing artwork" — your <itunes:image href> URL must be publicly accessible (not behind auth) and resolve to an image file with proper Content-Type. The Supabase public-bucket URLs satisfy this.
  • Apple feed rejection: "duplicate GUID" — each <item><guid> must be unique forever. We use season-{uuid}; never reuse a season UUID for a different episode.
  • "Failed to upload compiled video" — almost always the season-assets bucket's per-file size limit. Default Supabase limit is 50MB; rendered season videos are typically 80-200MB. Fix: Supabase dashboard → Storage → season-assets bucket → settings → set "File size limit" to 500MB or higher. The pipeline uses -preset veryfast -crf 28 to keep output file sizes reasonable, but a 75-min season at 1080p still lands around 120MB.
SEASON MEDIA PIPELINE — Docs | HiveJournal