The Inspiration Room (VR)
A hands-free, auto-advancing montage to start your day — uplifting quotes and affirmations, your own wins (coach encouragement + "on this day" memories + journal photos), and curated inspirational clips — drifting by under a starfield. "Put it on and get motivated."
Status: BUILT 2026-07-12. A new room in the
"HiveJournal in VR" hub, built on the shipped WebXR stack (StreamXRScene/EntryVrScene pattern,VrImage).
The experience
Route /dashboard/inspiration/vr
(also reachable from the VR hub card "Inspiration Room"). A single calm panel
floats at eye level under a procedural starfield; slides ease in one after
another every ~7s and loop. The camera is fixed (comfort rule: zero locomotion =
zero nausea); drag/gaze to look around, ‹ › to step, and a Pause/Play control.
Runs in the Quest browser ("Enter VR") and previews on desktop.
Content — a blend (universal + personal)
Assembled server-side by buildMontage(),
GET /api/inspiration/montage → { slides }. Curated house content is the
always-present backbone (so the room is never empty), with the viewer's own
material woven in among it (spread, not clustered), capped at ~16 slides:
| Slide kind | Source |
|---|---|
quote / affirmation | Curated house lists in inspiration.ts (widely-attributed lines + second-person affirmations) |
encouragement | The viewer's delivered (coach-approved) coaching feedback — reuses the coach-brief queue (coaching_seat_decisions) |
memory | "On this day" — prior-year journal entries matching today's MM-DD, with their first attached photo when present |
clip | Curated inspirational video (see below) |
Read-only, no LLM, no synthesis, no cost. Brand voice: transformation/becoming, never consumption metaphors.
Clips — hosted plays in-VR, YouTube is poster + out-link
Curated in an editable table, migration 479
inspiration_clips (add/edit rows without a deploy). Two kinds:
- Hosted (
video_url) — an MP4/HLS we host (or have rights to). Plays inside the room on a cinema screen viaTHREE.VideoTexture(real playback- audio; the tap that starts it is the required user gesture, and it pauses the
montage auto-advance until it ends). Add one by inserting a row with
video_url(+ optionalposter_url).
- audio; the tap that starts it is the required user gesture, and it pauses the
montage auto-advance until it ends). Add one by inserting a row with
- YouTube (
youtube_id) — external. The room shows the poster with a "▶ open on YouTube" out-link; the real iframe player embeds on the 2D room page (GET /api/inspiration/clips→ the gallery below the canvas).
Why not YouTube in the headset: you can't render a DOM <iframe> inside a
WebXR canvas, and YouTube's ToS forbids extracting their stream into a
VideoTexture. Hosted files are the only in-VR video path. The IFrame Player API
and Data API don't change this (iframe-only / metadata-only).
Curation UI (super-admin): /dashboard/admin/inspiration-clips
(also in the admin tools grid + ⌘K) — paste a YouTube URL (title + poster
auto-filled from oEmbed) or a hosted MP4/HLS URL (plays in-VR); show/hide
(is_active) or remove each. Endpoints GET|POST /api/inspiration/admin/clips,
PATCH|DELETE /api/inspiration/admin/clips/:id (super-admin), YouTube parsing via
utils/youtube.ts extractYouTubeVideoId
fetchYouTubeOEmbed.
⚠️ The seeded YouTube IDs are placeholders — video availability + embeddability change over time and are beyond the authoring assistant's knowledge cutoff. Review/curate the clips (via the admin page above) before relying on them.
Files
- Backend:
services/inspiration.ts(montage builder + curated content +listActiveClips),routes/inspiration.ts(/montage,/clips), mounted at/api/inspirationinindex.ts. - Migration:
479_inspiration_clips.sql. - Frontend:
components/inspiration-vr/InspirationVrScene.tsx(the WebXR scene +CinemaScreen),app/dashboard/inspiration/vr/page.tsx(page + 2D clips gallery). Registered in theVR hubROOMS.
Spoken montage (BUILT)
An opt-in 🔈 Narrate toggle reads each line aloud in the house voice (the
room narrator), so it becomes a truly hands-free/eyes-closed morning. Tapping the
toggle is the user gesture that unlocks audio (opt-in-to-play, never autoplay —
the Living Voice calm-not-noisy rule). When on, the montage paces to the
narration — each slide holds for the length of its spoken line and advances on
ended (a 25s safety timer if ended is missed); clips are never narrated (they
carry their own audio); a fixed dwell resumes when narration is off/unavailable.
POST /api/inspiration/narrate { text }→{ audio_url }reusesstudio-narrate.tsnarrateLine— render-once, content-hash cached in theseason-assetsbucket (STUDIO_HOUSE_VOICE_ID, Matilda). 204 when narration isn't configured → the room silently stays visual-only.- Cost-safe by construction: curated quotes are stable, so each renders once globally and every viewer replays the cached MP3; the scene only requests the current + next line (prefetch), so unreached lines are never synthesized.
- Now-playing card / glasses via
applyMediaSessionon the narration<audio>. - Which house voice — settable in the UI (no env/deploy): the platform
house-narrator is DB-backed (
site_settings.house_voice_id), resolved byservices/house-voice.tsgetHouseVoiceId()(DB →STUDIO_HOUSE_VOICE_IDenv → stock Matilda, 60s cache). A super-admin picks it on theclip-curation page— a curatedlistHostVoicesdropdown or a pasted ElevenLabs voice ID (GET|PUT /api/inspiration/admin/house-voice), effective immediately. The voice id is part of the narration cache key, so changing it renders fresh clips under new hashes (old clips untouched). Same voice Demo Studio narrates in. - Voice choice (BUILT): house voice (default, works for everyone) or the
viewer's own cloned voice — a 🎙 Your voice / 🏠 House voice picker that only
appears when a JQ-consented clone exists (
GET /api/inspiration/voice-readiness). Own voice routes throughsayInUserVoice(consent-v2 gated, per-userjq_voice_clipscache) so it's "your best self reading to you"; if the clone isn't set up,/narratetransparently falls back to the house voice so it always speaks.
Deploy gate
Apply migration 479 before the backend deploys — /api/inspiration/* SELECTs
inspiration_clips. Spoken montage needs an ElevenLabs key (else /narrate
returns 204 and the room stays visual-only); the house voice no longer needs
an env var — set it on the clip-curation admin page (falls back to
STUDIO_HOUSE_VOICE_ID / stock Matilda if unset).
Ambient bed (BUILT)
An opt-in 🎵 Ambience toggle plays a warm, asset-free synth pad under the
montage — a soft low chord (root/fifth/octave, lightly detuned) through a gentle
low-pass with a slow tremolo so it breathes, at low volume so it sits under the
narration. lib/useAmbientPad.ts
(Web Audio, no MP3/CSP; the AudioContext is created only inside the toggle's user
gesture — opt-in-to-play, never autoplay). Mirrors useAmbientRiver.
Not yet built / next
- Glasses (audio-first) — the spoken-only montage is a natural Wave-1 fit
(a "play my inspiration montage" voice trigger on
apps/glasses). - ⌘K palette entry for the room itself (it's in the VR hub + features catalog today).