product

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 kindSource
quote / affirmationCurated house lists in inspiration.ts (widely-attributed lines + second-person affirmations)
encouragementThe 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
clipCurated inspirational video (see below)

Read-only, no LLM, no synthesis, no cost. Brand voice: transformation/becoming, never consumption metaphors.

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 via THREE.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 (+ optional poster_url).
  • 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/inspiration in index.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 the VR hub ROOMS.

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 } reuses studio-narrate.ts narrateLinerender-once, content-hash cached in the season-assets bucket (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 applyMediaSession on 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 by services/house-voice.ts getHouseVoiceId() (DB → STUDIO_HOUSE_VOICE_ID env → stock Matilda, 60s cache). A super-admin picks it on the clip-curation page — a curated listHostVoices dropdown 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 through sayInUserVoice (consent-v2 gated, per-user jq_voice_clips cache) so it's "your best self reading to you"; if the clone isn't set up, /narrate transparently 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).
INSPIRATION ROOM — Docs | HiveJournal