| name | cinema-voice-architect |
| description | Expert architect for Cinema Mode, Discipleship Cinema, and audio/voice systems in Raamattu Nyt.
Use when:
(1) Building or modifying Cinema Mode full-screen verse presentation
(2) Implementing audio playback with verse-level synchronization
(3) Integrating ElevenLabs TTS voice generation
(4) Creating auto-scroll functionality with or without audio
(5) Managing audio cues and timing synchronization
(6) Working with background music and visual effects (Ken Burns)
(7) Implementing dual-track audio (Bible + music) controls
(8) Building or modifying Discipleship Cinema Mode (task selector, inline task, verse bar, transition overlay, pomodoro)
(9) Working with CinemaShell base layer (background, music, fullscreen, preferences)
(10) Integrating practice/prayer tasks into Cinema full-screen flow
(11) Working with Nyt Kooste (daily verse summary collection) in cinema
(12) Implementing verse memory quizzes (VerseMemoryQuiz, reading plan memory, smart verse selection)
(13) Working with TanaanPage sections (TodayTaskList, TomorrowTasks, PermanentTasks, RecurringTasks)
(14) Working with curated grand plan programs (CuratedPlanCinema, CuratedPlanFlow, ScrollingPlanFlow, MiniTaskView) and progression modes (continuous / linear / scrolling)
(15) Working with useDiscipleshipOrchestration, useDiscipleshipTasks, useNytSummary hooks
(16) Configuring autoStart reading plan cinema mode vs full discipleship cinema
(17) Building new CinemaShell consumers (prayer rooms, meditation, etc.)
(18) Working with Prayer Room (Rukoushuone) — realtime multiplayer full-screen prayer
(19) Implementing push-to-talk (PTT), hand-raise queue, or WebRTC audio mesh
(20) Working with prayer room host/participant sync, invitations, or calendar-sourced prayers
Triggers: "cinema mode", "voice playback", "audio sync", "verse scrolling",
"ElevenLabs", "TTS", "audio cues", "auto-advance", "full screen reader",
"background music", "Ken Burns", "verse timing", "discipleship cinema",
"CinemaShell", "CinemaReaderScreen", "useDiscipleshipOrchestration",
"discipleship-tila", "DiscipleshipVerseBar", "DiscipleshipInlineTask",
"DiscipleshipTaskSelector", "pomodoro break", "inline task", "task selector",
"transition overlay", "useTodayTasks", "UnifiedTodayTask", "Nyt Kooste",
"CuratedPlanCinema", "CuratedPlanFlow", "ScrollingPlanFlow", "MiniTaskView", "mini-task",
"ohjelma", "grand plan cinema", "curated plan", "skrollaava ohjelma", "scrolling plan",
"progression_type", "GrandPlanProgressionType", "continuous", "linear", "scrolling",
"progression mode", "etenemistapa", "snap-scroll steps", "auto-advance timer",
"MiniTaskContentConfig", "MiniTaskRoute", "MiniTaskChoice", "duration_seconds",
"jump_to_sort_order", "complete_grand_plan", "useCuratedPlanCinema", "grand plan completion",
"mini_task_config", "grand_plan_sort_order",
"useReadingPlanQuiz", "useSmartVerseSelection", "useDiscipleshipTasks",
"useNytSummary", "TanaanPage", "tanaan", "opetuslapseus", "discipleship landing",
"initialTask", "nytKoosteRefs", "reading_plan_memory", "verse_memory", "read_memory",
"autoStart", "reading plan cinema", "marked_refs", "dimControls", "hideControls",
"CinemaShellContext", "render props",
"rukoushuone", "prayer room", "PrayerRoomScreen", "PrayerRoomSetup",
"PrayerRoomContent", "PrayerRoomHeader", "PrayerRoomBottomBar",
"PrayerRoomInviteDialog", "CalendarPrayerBrowser", "useTodayPrayerRoom",
"usePrayerRooms", "usePrayerRoomSync", "usePrayerRoomInvitations",
"usePushToTalk", "useWebRTCAudio", "push-to-talk", "PTT", "hand queue",
"raise hand", "grant talk", "release talk", "WebRTC mesh", "audio mesh",
"multiplayer prayer", "host sync", "prayer_rooms", "prayer_room_invitations",
"invite code", "calendar prayers", "päivän rukoushuone", "mutedCalendarIds",
"useMyCalendar", "useSubscribedCalendars", "prayer room realtime"
|
Cinema Voice Architect
Expert skill for Cinema Mode and audio/voice implementation.
Quick Reference
Base Layer (CinemaShell)
| Component | Location |
|---|
| CinemaShell | src/components/cinema/CinemaShell.tsx |
| Cinema Background | src/components/cinema/CinemaBackground.tsx |
| Background Music Picker | src/components/cinema/BackgroundMusicPicker.tsx |
| Background Visual Picker | src/components/cinema/BackgroundVisualPicker.tsx |
| Cinema Preferences Hook | src/hooks/useCinemaPreferences.ts |
| Cinema Fullscreen Hook | src/hooks/useCinemaFullscreen.ts |
| Cinema Audio Hook | src/hooks/useCinemaAudio.ts |
Content Layer (CinemaReaderScreen)
| Component | Location |
|---|
| CinemaReaderScreen | src/features/cinema/CinemaReaderScreen.tsx |
| Bible Audio Hook | src/hooks/useBibleAudio.ts |
| Auto-Advance Hook | src/hooks/useAutoAdvance.ts |
| Chapter Bundle Hook | src/hooks/useChapterBundle.tsx |
| Audio Sync | src/lib/cinemaAudioSync.ts |
| Audio Estimation | src/lib/audioEstimation.ts |
| Cinema Types | src/types/cinema.ts |
Discipleship Layer
| Component | Location |
|---|
| Discipleship Orchestration | src/hooks/useDiscipleshipOrchestration.ts |
| Discipleship Utils | src/components/cinema/discipleshipUtils.tsx |
| Discipleship TaskSelector | src/components/cinema/DiscipleshipTaskSelector.tsx |
| Discipleship InlineTask | src/components/cinema/DiscipleshipInlineTask.tsx |
| Discipleship VerseBar | src/components/cinema/DiscipleshipVerseBar.tsx |
| Discipleship Pomodoro | src/components/cinema/DiscipleshipPomodoroButton.tsx |
| Discipleship Transition | src/components/cinema/DiscipleshipTransitionOverlay.tsx |
| Verse Memory Quiz | src/components/practice/VerseMemoryQuiz.tsx |
| Discipleship Tasks Hook | src/hooks/useDiscipleshipTasks.ts |
| Nyt Summary Hook | src/hooks/useNytSummary.ts |
| Reading Plan Quiz Hook | src/hooks/useReadingPlanQuiz.ts |
| Smart Verse Selection | src/hooks/useSmartVerseSelection.ts |
Program Mode (Curated Plans)
| Component | Location |
|---|
| CuratedPlanCinema (wrapper + flow router) | src/components/cinema/CuratedPlanCinema.tsx |
CuratedPlanFlow (continuous / linear) | src/components/cinema/CuratedPlanFlow.tsx |
ScrollingPlanFlow (scrolling) | src/components/cinema/ScrollingPlanFlow.tsx |
| MiniTaskView (inside CuratedPlanFlow) | src/components/cinema/MiniTaskView.tsx |
| Curated Plan Cinema Hook | packages/shared-practices/src/hooks/useCuratedPlanCinema.ts |
Shared types (GrandPlanProgressionType, MiniTaskContentConfig, MiniTaskRoute) | packages/shared-practices/src/types.ts |
Prayer Room (Rukoushuone)
| Component / Hook | Location |
|---|
| PrayerRoomScreen | src/features/prayer-room/PrayerRoomScreen.tsx |
| PrayerRoomPage (route) | src/pages/PrayerRoomPage.tsx |
| PrayerRoomSetup | src/features/prayer-room/PrayerRoomSetup.tsx |
| PrayerRoomContent | src/features/prayer-room/PrayerRoomContent.tsx |
| PrayerRoomHeader | src/features/prayer-room/PrayerRoomHeader.tsx |
| PrayerRoomBottomBar | src/features/prayer-room/PrayerRoomBottomBar.tsx |
| PrayerRoomInviteDialog | src/features/prayer-room/PrayerRoomInviteDialog.tsx |
| CalendarPrayerBrowser | src/features/prayer-room/CalendarPrayerBrowser.tsx |
| Prayer Room Types | src/features/prayer-room/types.ts |
| useTodayPrayerRoom | src/features/prayer-room/useTodayPrayerRoom.ts |
| usePrayerRooms | src/hooks/usePrayerRooms.ts |
| usePrayerRoomSync (Realtime) | src/hooks/usePrayerRoomSync.ts |
| usePrayerRoomInvitations | src/hooks/usePrayerRoomInvitations.ts |
| usePushToTalk | src/hooks/usePushToTalk.ts |
| useWebRTCAudio | src/hooks/useWebRTCAudio.ts |
Audio Pipeline
| Component | Location |
|---|
| Audio Service | src/lib/audioService.ts |
| ElevenLabs Voices | src/lib/elevenLabsVoices.ts |
| Audio Generation | supabase/functions/generate-audio/index.ts |
Pages
| Component | Location |
|---|
| Tanaan Page | src/pages/TanaanPage.tsx |
| Discipleship Landing | src/pages/DiscipleshipLandingPage.tsx |
| Spiritual Path Faith | src/pages/SpiritualPathFaithPage.tsx |
| Daily Reading View | src/components/reading-plans/DailyReadingView.tsx |
Architecture Overview
CinemaShell (base layer ~438 lines)
│ Fullscreen, background visuals (Ken Burns), background music,
│ preferences, visual/music pickers, keyboard shortcuts (B/N/V/M)
│ Props: isOpen, onClose, title?, hideControls?, dimControls?
│ Exports: CinemaShellContext (render props for children)
│
├── CinemaReaderScreen (content layer ~1033 lines)
│ │ Verse fetching/mapping, Bible audio, auto-advance, audio sync,
│ │ completion overlay. Uses CinemaShell as wrapper via render props.
│ │ Modes: "chapter" | "verseList" | "summaryItems"
│ │
│ └── useDiscipleshipOrchestration (task queue ~792 lines)
│ │ Task queue, completion persistence, quiz insertion,
│ │ transition overlays, verse bar, kooste, prayer messages
│ │
│ └── Overlay Components
│ ├── DiscipleshipTaskSelector (pick tasks)
│ ├── DiscipleshipInlineTask (prayer/practice/quiz)
│ ├── DiscipleshipTransitionOverlay (between tasks)
│ └── DiscipleshipVerseBar (note/share on pause)
│
├── CuratedPlanCinema (program mode ~66 lines)
│ │ Wrapper: loads grand plan mini_tasks + progression_type via useCuratedPlanCinema.
│ │ CinemaShell with hideControls. Routes to flow based on progression_type:
│ │ progressionType === "scrolling" ? ScrollingPlanFlow : CuratedPlanFlow
│ │ On all-tasks-complete: calls complete_grand_plan RPC + invalidates ["grand-plans"].
│ │
│ ├── CuratedPlanFlow (continuous / linear ~133 lines)
│ │ │ One mini-task card at a time, AnimatePresence crossfade.
│ │ │ Scrim bg-black/40 + card bg-black/60 backdrop-blur-md.
│ │ │ completedIds Set; backward jumps un-complete the target.
│ │ │ Choice routing: next | jump_to_sort_order.
│ │ │
│ │ └── MiniTaskView (step UI ~223 lines)
│ │ SVG circular countdown timer, pause via DiscipleshipPomodoroButton,
│ │ JATKA hidden when choices exist (choice-to-advance).
│ │
│ └── ScrollingPlanFlow (scrolling ~336 lines)
│ Snap-scroll all steps on one page, numbered progress pills (click-to-jump),
│ IntersectionObserver (threshold 0.4) tracks active step,
│ per-step auto-advance timer (duration_seconds, default 30s),
│ user-scroll detection pauses auto-advance 1.5s.
│ Inline StepContent (NOT MiniTaskView) — no countdown ring, no completedIds.
│ See references/curated-plans.md for full details.
│
├── PrayerRoomScreen (prayer room consumer ~499 lines)
│ │ CinemaShell with hideControls; owns prayer/verse state, realtime sync,
│ │ PTT, WebRTC audio mesh, invitations. Route: /rukoushuone.
│ │ See references/prayer-room.md for full details.
│ │
│ └── Parallel runtime systems (scoped to config.id)
│ ├── usePrayerRoomSync (presence + host state broadcast, debounced 100ms)
│ ├── usePushToTalk (single-speaker floor + FIFO hand queue)
│ └── useWebRTCAudio (STUN-only audio mesh, 2–5 users)
│
└── Future: Meditation, etc.
└── CinemaShell + custom content
Audio Pipeline:
generate-audio Edge Function → ElevenLabs API (with timestamps)
→ audio_assets table (hash-cached) → audio_cues table (verse timing)
Audio Split
CinemaShell and CinemaReaderScreen both use useCinemaAudio but for different purposes:
- CinemaShell:
bibleAudioUrl: null (music-only)
- CinemaReaderScreen:
backgroundMusicUrl: null (Bible audio-only, music from CinemaShell context)
Cinema Mode Components
CinemaShell
Reusable base layer at src/components/cinema/CinemaShell.tsx (~438 lines):
CinemaReaderScreen
Content layer at src/features/cinema/CinemaReaderScreen.tsx (~1033 lines):
Mobile UI (Compact Mode)
On phones (useBreakpoint().isPhone || isCozy, <768px) Cinema Mode collapses the desktop two-row control panel + top-left HUD pills into a single row + two icon-triggered bottom sheets, freeing ~90px for the verse text.
🔊 icon → CinemaMusicPopover (track, favorite, volume, favorites-only, change track)
⚙ icon → CinemaSettingsSheet (background, completion mode, speed, voice volume + on/off)
- Adaptive verse typography via CSS
clamp() + vw (3 tiers; small-phone ≤380px gets 24-30px to fit Acts 1:14 on iPhone SE)
- Sheets render inside native fullscreen via
getCinemaPortalContainer() (Radix portal targeting fix)
Critical: @media (max-width: 479px) matches iPhone 14 Pro Max (430px). Use max-width: 380px for genuine "small phone" rules.
For full details (component contracts, control distribution table, font tier table, portal pattern, gotchas): See references/mobile-ui.md
useDiscipleshipOrchestration
Task queue hook at src/hooks/useDiscipleshipOrchestration.ts (~792 lines):
Animation Modes
Five animation modes in src/types/cinema.ts: slide, zoom, stack, loopH, loopV
Ken Burns Effect
CinemaBackground.tsx: Random transform over 25s (scale 1.0-1.15, translate ±5%)
Audio System
ElevenLabs Integration
Voices in src/lib/elevenLabsVoices.ts: Venla (female, T5qAFgaL2uYxoUtojUzQ), Urho (male, 1WVCONUwYGulVaKg4oTr)
Audio Generation Flow
Client → Edge Function → check hash cache → ElevenLabs API (with timestamps)
→ parse timestamps → verse cues → store MP3 → save metadata + cues → return
For detailed API reference, see references/elevenlabs-api.md
Audio Cue Format
See references/audio-cue-format.md for full specification.
Auto-Advance (Without Audio)
useAutoAdvance: WPM-based timing (default 150, adjustable 50-400). Min 1.5s per verse.
Priority: audio cues > auto-advance timer.
Dual-Track Audio
useCinemaAudio: Bible + background music tracks with independent volume (0-1).
Database Schema
See existing tables: audio_assets, audio_cues, cinema_preferences, background_tracks, background_visuals in bible_schema.
Discipleship Cinema Mode
Immersive full-screen task flow launched from Tanaan page. Guides users through daily reading plans, prayers, practices, and memory quizzes sequentially.
For full details: See references/discipleship-cinema.md
Key Concepts
- Launched via "Discipleship-tila" button on
/tanaan → CinemaReaderScreen(mode="verseList")
useTodayTasks normalizes practices + plans + prayers → UnifiedTodayTask[]
useDiscipleshipTasks convenience hook composes all data sources
- Task routing:
reading_plan → verse reader, prayer/practice → DiscipleshipInlineTask
- Quiz content types:
verse_memory, read_memory, reading_plan_memory → VerseMemoryQuiz
- Between tasks:
DiscipleshipTransitionOverlay with progress bar + free navigation
DiscipleshipVerseBar on pause: Add to Nyt Kooste, Note, Share, Pomodoro Break
- Nyt Kooste: Auto-created daily verse collection, appended as final task
- Reading Plan Memory Quiz: Auto-inserted after each reading plan day completion (discipleship mode only, NOT autoStart)
- initialTask prop: Start specific task immediately, bypass selector
- autoStart prop: Skip task selector, auto-queue all uncompleted tasks, auto-skip transitions
- Break duration from
app_config.discipleship_break_minutes (default 5 min)
Two Cinema Launch Modes from Tanaan
| Mode | Button | Props | Behavior |
|---|
| Reading Plan Cinema | Blue Play button | discipleshipTasks={readingPlanCinemaTasks} autoStart | Auto-plays reading plans only. No task selector, no discipleship UI toggle, no memory quizzes, no transition overlays. |
| Discipleship Cinema | Amber Clapperboard button | discipleshipTasks={allTasks} | Full discipleship flow: task selector → reading + prayer + practice → quizzes → kooste. |
autoStart behavior:
- Queues all uncompleted tasks, starts first immediately (line ~1071-1092)
- Hides discipleship toggle/next button in CinemaReader (
discipleshipMode={false}, no onDiscipleshipToggle)
- Skips reading plan memory quizzes (
!props.autoStart guard on quiz insertion)
- Auto-skips transition overlays between tasks (useEffect at line ~1285-1290)
- When all tasks done: calls
onAllTasksCompleted and closes
Reading Plan Completion
handleComplete calls mark_reading_day_complete RPC when a reading plan finishes. Key details:
- Completion-type plans: RPC advances
current_day immediately (e.g. 5→6)
- Dashboard fix:
get_today_dashboard checks completed_at::date = CURRENT_DATE for completion-type plans (not current_day which already advanced)
- Client-side fix:
useTodayTasks checks completed_days.includes(current_day - 1) for completion-type plans as fallback
- Query invalidation: Must invalidate
["today-dashboard"], ["reading-plan-streaks"], AND ["user-reading-plans"]
Reading Plan Memory Quiz
useReadingPlanQuiz generates a 3-verse quiz after reading plan completion:
- Picks 2 plan verses + 1 decoy (from user highlights or well-known verses)
- Marked verse priority: Verses user marked via DiscipleshipVerseBar (
marked_refs in quiz task metadata) are selected first
- Priority: marked refs in plan → preferred books (NT, Psalms, Proverbs) → any plan verse
- Quiz task metadata includes
marked_refs: string[] populated from nytKoosteRefsRef.current
Tanaan Page Sections
| Section | Component | Content |
|---|
| Päivän tehtävät | TodayTaskList | Uncompleted + completed tasks with quick complete |
| Huomisen tehtävät | TomorrowTasksSection | Next day preview (collapsible) |
| Pysyvät tehtävät | PermanentTasksSection | Prayers with priority_level='always' |
| Toistuvat tehtävät | RecurringTasksSection | Active plans + subscribed prayer calendars |
| Tilaa harjoitus | PracticeActivationCard | Available templates to subscribe |
DiscipleshipLandingPage
Marketing page at /opetuslapseus (fi) / /en/discipleship (en). Four pillars: Read, Pray, Reflect, Practice. JSON-LD SEO (WebPage + FAQPage).
Discipleship Files
src/components/cinema/
├── DiscipleshipTaskSelector.tsx # Task picker before cinema
├── DiscipleshipInlineTask.tsx # Full-screen prayer/practice/quiz view
├── DiscipleshipVerseBar.tsx # Verse action bar (note, share, break, add to kooste)
├── DiscipleshipPomodoroButton.tsx # Break timer
└── DiscipleshipTransitionOverlay.tsx # Between-task progress + free navigation
src/components/practice/
└── VerseMemoryQuiz.tsx # Quiz for verse/reading plan memory
src/components/today/
├── TodayTaskList.tsx # Päivän tehtävät section
├── TomorrowTasksSection.tsx # Tomorrow preview
├── PermanentTasksSection.tsx # Always-priority prayers
└── RecurringTasksSection.tsx # Active plans + prayer calendars
src/hooks/
├── useDiscipleshipTasks.ts # Convenience wrapper for all task sources
├── useNytSummary.ts # Daily verse summary collection
├── useReadingPlanQuiz.ts # Generate quiz from reading plan day
└── useSmartVerseSelection.ts # Priority-based verse picking for quizzes
Curated Plans (Grand Plans) & Progression Modes
Curated grand plans are a second CinemaShell consumer. The wrapper CuratedPlanCinema routes to one of two flow components based on the plan's progression_type, then renders inside CinemaShell hideControls.
For full details: See references/curated-plans.md
Progression Types
type GrandPlanProgressionType = 'continuous' | 'linear' | 'scrolling';
| Value | Flow Component | UX |
|---|
continuous (default) | CuratedPlanFlow | One mini-task card at a time + countdown timer |
linear | CuratedPlanFlow | Same code path as continuous — semantic distinction only |
scrolling | ScrollingPlanFlow | All steps on one snap-scrollable page with auto-advance |
Flow router (CuratedPlanCinema.tsx line ~55):
const FlowComponent = progressionType === "scrolling" ? ScrollingPlanFlow : CuratedPlanFlow;
Both flows share the same props contract: { tasks, onAllTasksCompleted, onClose }.
CuratedPlanFlow (continuous / linear)
- One mini-task card at a time via
AnimatePresence mode="wait" crossfade
- Scrim
bg-black/40 + card bg-black/60 backdrop-blur-md max-w-md
- Uses
<MiniTaskView> (SVG circular countdown timer, pause via DiscipleshipPomodoroButton)
completedIds: Set<string> tracks done tasks; advance skips already-done
- Backward jump via choice → un-completes the target (allows replay)
- X button = skip WITHOUT marking complete
- Choice routing:
{type:'next'} | {type:'jump_to_sort_order', sort_order} — sort_order matched against task.metadata.grand_plan_sort_order
ScrollingPlanFlow (scrolling)
- All steps rendered on a single snap-scrollable page (
snap-y snap-mandatory)
- Numbered progress pills at top (click-to-jump), amber = active, emerald = past, faint = future
IntersectionObserver (threshold: [0, 0.25, 0.5, 0.75, 1], root = container) tracks active step; gates state update at ratio > 0.4 to prevent flicker
- Per-step auto-advance:
duration_seconds (default 30s) → scrollToStep(i+1) unless user is scrolling
- User-scroll detection: any
scroll event flips userScrollingRef = true with 1500ms debounce — pauses auto-advance
- Scrim
bg-black/50 (stronger than CuratedPlanFlow)
- Uses its own inline
StepContent (NOT MiniTaskView) — no countdown ring, no completedIds
JATKA (non-last) = scroll, LOPETA (last) = finish + close
MiniTaskContentConfig (per-step data)
interface MiniTaskContentConfig {
title: string;
body: string;
icon?: string;
duration_seconds: number;
choices?: MiniTaskChoice[];
verse_ref?: string;
closing_text?: string;
image_url?: string;
show_break_button?: boolean;
}
Plan Completion
- Last task / LOPETA → flow calls
onAllTasksCompleted() then onClose() synchronously
CuratedPlanCinema.handleAllTasksCompleted:
- Sets
completedRef.current = true BEFORE the async RPC (close fires synchronously after)
- Calls
supabase.rpc('complete_grand_plan', { p_grand_plan_id: planId })
- Invalidates
["grand-plans"]
handleClose → onClose(), then if completedRef && completionRedirectUrl → navigate(url) after 300ms
Critical Gotchas
continuous and linear are identical in code. Only "scrolling" branches — the other two both fall through to CuratedPlanFlow. Branch INSIDE the flow if you need them to differ, don't split the router.
- ScrollingPlanFlow reimplements step UI. Inline
StepContent is a simpler cousin of MiniTaskView (no timer, no pause). Behavioral changes to MiniTaskView do NOT propagate — fix both or refactor to share.
- Completion order is load-bearing.
completedRef.current = true must run BEFORE the await complete_grand_plan RPC — flow calls onAllTasksCompleted then onClose synchronously, and handleClose reads the ref immediately.
- Two RPCs on mount.
useCuratedPlanCinema fires get_grand_plan_items AND get_user_grand_plans (the latter only for progression_type). Consider folding progression_type into items if you refactor.
- Default progression falls back to
continuous. When the user isn't a member yet, get_user_grand_plans has no match → silent continuous default. Worth checking first when a plan "won't scroll".
- ScrollingPlanFlow doesn't invoke
complete_grand_plan. That's CuratedPlanCinema's job. A new flow must call onAllTasksCompleted() at the end, or the plan never marks complete.
- Intersection threshold 0.4 and user-scroll debounce 1500ms are tuned. Lowering the threshold flickers active-index; shortening the debounce fires auto-advance mid-flick. Don't "clean up" these constants.
Prayer Room (Rukoushuone)
Realtime multiplayer full-screen prayer experience built on CinemaShell (not CinemaReader). Canonical example of a CinemaShell-only consumer: reuses fullscreen + Ken Burns + background music + preferences, adds its own content, host-driven realtime sync, push-to-talk, and WebRTC audio mesh on top.
For full details: See references/prayer-room.md
Key Concepts
- Route:
/rukoushuone → PrayerRoomPage → PrayerRoomScreen(initialConfig?, onClose)
- Entry helpers:
useTodayPrayerRoom opens stored room → fallback most recent → fresh "Päivän rukoushuone"
- Two views:
"setup" (form) and "room" (CinemaShell hideControls + render props)
isHost = !config.id || !user ? true : config.hostUserId === user.id — solo sessions and anonymous users are always host
- Content sources (merged + deduped by
baseId = id.replace(/^(cal-|mine-|sub-)/, "")):
- Ad-hoc
config.prayers (uuid ids)
useMyCalendar → user's own + followed active prayers (mine-<id>)
useSubscribedCalendars minus mutedCalendarIds (sub-<id>)
config.calendarId extra browsing calendar (cal-<id>)
- Legacy rooms normalize
useMyCalendar/useSubscribedCalendars to true; new rooms default toggles to false in setup — asymmetric by design
- Host keyboard: ArrowLeft/Right (prayer nav), ArrowUp/Down (calendar day, only if
config.calendarId)
- Verses: OSIS is canonical removal key; adds and removes auto-persist when
config.id exists
Realtime Multiplayer
Channel: prayer-room:<roomId> via Supabase Realtime.
- Presence (keyed by user.id):
displayName, avatarUrl fetched from profiles before tracking
- Broadcast "sync": Host sends
{ currentPrayerIndex, currentVerseIndex, calendarDayOffset } debounced 100ms; participants apply in useEffect gated on !isHost && syncState
- Push-to-talk (PTT): Single speaker at a time; FIFO
handQueue; host grants via avatar click or autoGrantNext on release/leave; events: raise_hand, grant_talk, release_talk
- WebRTC audio mesh: STUN-only (
stun:stun.l.google.com:19302), 2–5 users, one RTCPeerConnection + <audio> per peer, signaling over the same channel; micOpen = activeSpeakerId === userId && pttPressed toggles track enable
- Channel ownership:
usePrayerRoomSync owns subscribe/unsubscribe. usePushToTalk and useWebRTCAudio attach listeners but do NOT unsubscribe.
Invitations
Table public.prayer_room_invitations with invitee_id OR invitee_email, status pending|accepted|declined. Invite flow auto-persists the room first via createRoom.mutateAsync when opening the invite dialog from an unsaved config.
Critical Gotchas
- Modals inline, not portaled. Setup, invite, verse-full popup, and remove-verse confirmation render as
absolute inset-0 z-[10003] children of the CinemaShell subtree. Do NOT use shadcn AlertDialog or Dialog — both auto-portal via Radix to document.body and become invisible inside the fullscreen element. asDialog / inline props on child dialogs exist for this. See references/learnings.md → "Radix Dialog/AlertDialog Auto-Portals" for the full pattern.
- Z-index stack: CinemaShell base < Header/BottomBar
z-[10002] < inline modals z-[10003]. Don't invent new values.
- Title pill is rendered by PrayerRoomHeader, not CinemaShell. Pass NO
title prop to <CinemaShell> from PrayerRoomScreen — the header renders the pill on its own row 1 alongside the prayer-count nav. Adding the title back to CinemaShell yields a stacked duplicate.
calendarName already includes "Rukouskalenteri" prefix. It comes from the DB name column (e.g. "Rukouskalenteri Suomi"). Don't prepend prayerRoom.header.calendarPrefix again — that produced the duplicate "Rukouskalenteri Rukouskalenteri Suomi" bug. The Calendar icon next to the name carries the semantics.
- Prayer typography matches
.cinema-verse-text. PrayerRoomContent's prayer body uses clamp(36px, 5vw, 56px) + min(1100px, 88vw) lane — exactly the same as cinema-reader's verse text (packages/cinema-reader/src/styles/cinema.css). The middle container must NOT have max-w-2xl mx-auto or any narrower cap that would clip this lane.
src/hooks/usePrayerRoom.ts is unused. PrayerRoomScreen manages state inline; do not "refactor to use the hook" without migrating sync/PTT/WebRTC plumbing too.
prayer_rooms / prayer_room_invitations not in types.ts — hooks still use (supabase as any). When types regenerate, drop the casts (see /supabase-typing-architect).
- STUN-only mesh fails on symmetric NAT / strict firewalls. Add TURN + SFU if scaling beyond 5 users.
Common Tasks
Add New Voice
- Get voice ID from ElevenLabs → add to
elevenLabsVoices.ts → add to admin UI → use elevenlabs:{voiceId} format
Debug Audio Sync
Add interval logging in useCinemaAudio to trace findCurrentCue() output.
File Structure
src/
├── features/cinema/
│ └── CinemaReaderScreen.tsx # Main orchestrator
├── components/cinema/
│ ├── CinemaBackground.tsx # Video/image + Ken Burns
│ ├── BackgroundMusicPicker.tsx # Music selection UI
│ ├── BackgroundVisualPicker.tsx # Visual selection UI
│ ├── DiscipleshipTaskSelector.tsx
│ ├── DiscipleshipInlineTask.tsx
│ ├── DiscipleshipVerseBar.tsx
│ ├── DiscipleshipPomodoroButton.tsx
│ └── DiscipleshipTransitionOverlay.tsx
├── components/today/
│ ├── TodayTaskList.tsx
│ ├── TomorrowTasksSection.tsx
│ ├── PermanentTasksSection.tsx
│ └── RecurringTasksSection.tsx
├── components/practice/
│ └── VerseMemoryQuiz.tsx
├── hooks/
│ ├── useCinemaAudio.ts # Dual-track audio
│ ├── useBibleAudio.ts # Single-track audio
│ ├── useAutoAdvance.ts # WPM-based scrolling
│ ├── useCinemaPreferences.ts # Preferences persistence
│ ├── useCinemaFullscreen.ts # Fullscreen API
│ ├── useChapterBundle.tsx # Bundle with audio data
│ ├── useDiscipleshipTasks.ts
│ ├── useNytSummary.ts
│ ├── useReadingPlanQuiz.ts
│ └── useSmartVerseSelection.ts
├── lib/
│ ├── audioService.ts # Audio generation client
│ ├── elevenLabsVoices.ts # Voice configurations
│ ├── cinemaAudioSync.ts # Cue-to-verse mapping
│ └── audioEstimation.ts # Word count timing
├── types/
│ ├── cinema.ts # Cinema types & defaults
│ └── reel.ts # Reel rendering types
├── pages/
│ ├── TanaanPage.tsx
│ └── DiscipleshipLandingPage.tsx
supabase/functions/
└── generate-audio/
└── index.ts # ElevenLabs Edge Function
packages/shared-practices/src/hooks/
└── useTodayTasks.ts # Unified task normalization
References
references/audio-cue-format.md - Detailed cue timing specification
references/elevenlabs-api.md - ElevenLabs API reference
references/discipleship-cinema.md - Full discipleship cinema mode documentation (components, hooks, flows, gotchas)
references/reading-plan-transition.md - State flow for "Next Day" in reading plan cinema mode
references/prayer-room.md - Prayer Room (Rukoushuone): CinemaShell consumer with realtime sync, PTT, WebRTC audio mesh, invitations
references/curated-plans.md - Curated grand plans: progression modes (continuous / linear / scrolling), flow routing, completion RPC, MiniTask data model
references/mobile-ui.md - Cinema Mode mobile UI: compact controls (🔊 + ⚙ sheets), adaptive verse typography (clamp+vw, ≤380px tier), native fullscreen portal pattern, gotchas
references/learnings.md - Bug patterns and fixes
Cross-cutting learnings: See .claude/LEARNINGS.md → "CSS/Layout" section for framer-motion patterns and animation gotchas.