Lucid dream induction app via sleep-aware audio prompting. Expo/React Native cross-platform.
npm run web # Dev server at localhost:8081
npm run ios # iOS simulator
npm run android # Android emulator
npm run typecheck # TypeScript validation
# Dream content pipeline (Python)
python scripts/build_dream_data.py # Rebuild dreamData.ts from narratives
python scripts/generate_audio.py --dream dream-24 # Generate audio for single dream
python scripts/generate_audio.py --all # Generate audio for all dreams
python scripts/generate_audio.py --start 24 # Generate audio for dreams 24+| Layer | Technology |
|---|---|
| Framework | Expo SDK 52+, Expo Router |
| UI | React Native, NativeWind (Tailwind) |
| Audio | expo-av, Meyda (audio analysis) |
| Animation | react-native-reanimated |
| Backend | Supabase (currently mock data) |
| TTS | Edge TTS (CI), en-GB-SoniaNeural |
| Music | Procedural synthesis (numpy) |
┌─────────────────────────────────────────────────────────┐
│ USER INTERFACE │
│ app/(tabs)/ Expo Router screens │
│ components/ React components + NativeWind │
├─────────────────────────────────────────────────────────┤
│ STATE & HOOKS │
│ hooks/ React hooks (useAuth, useDreams, etc) │
├─────────────────────────────────────────────────────────┤
│ BUSINESS LOGIC │
│ services/ Core services (sleep, playback, auth) │
│ lib/ Data, storage, constants │
├─────────────────────────────────────────────────────────┤
│ TOOLING │
│ scripts/ Audio generation (Python) │
│ .github/ CI/CD workflows │
└─────────────────────────────────────────────────────────┘
app/ # Expo Router screens
(tabs)/ # Tab navigation (home, dream, queue, favorites, settings)
auth/ # Authentication screens
dream/ # Dream detail/player ([id].tsx, launch.tsx)
about.tsx # App info and instructions
components/ # React components
ui/ # Base components (Button, Card, Input, Text, MarqueeText)
DreamCard.tsx # Dream list item with queue/favorite/play actions
DreamPlayer.tsx # Audio player with controls
SleepModePlayer.tsx # REM-aware playback during sleep
VolumeSetup.tsx # Volume calibration before sleep mode
hooks/ # React hooks
useDreams.ts # Dream data fetching/search
useSleepTracking.ts # Sleep session management
useLaunchQueue.ts # Queue state (shared across components)
useFavorites.ts # Favorites persistence
services/ # Business logic (see services/AGENTS.md)
lib/ # Utilities
dreamData.ts # Generated from narratives (DO NOT EDIT DIRECTLY)
storage.ts # AsyncStorage wrapper
constants.ts # App constants and thresholds
scripts/ # Build tools (see scripts/AGENTS.md)
narratives/ # Dream narrative text files + metadata.json
build_dream_data.py # Generates lib/dreamData.ts from narratives
generate_audio.py # TTS narration generation
generate_music.py # Procedural music + mixing
theme/ # Design tokens (colors, spacing, typography)
types/ # TypeScript type definitions
public/audio/ # Generated audio files
dreams/ # {id}_full.opus, {id}_combined.opus, {id}_preview.opus
music/ # {id}_music.opus
// Dynamic URL based on deployment context
const baseUrl =
typeof window !== 'undefined'
? `${window.location.origin}${window.location.pathname.replace(/\/[^/]*$/, '')}`
: '';
const audioUrl = `${baseUrl}/audio/dreams/${dreamId}_combined.opus`;The app uses a hybrid classifier that fuses audio-based breathing analysis with wearable vitals (via HealthConnect/HealthKit):
┌─────────────────────────────────────────────────────────────────┐
│ SLEEP SESSION START │
├─────────────────────────────────────────────────────────────────┤
│ 1. Load existing model (or create empty) │
│ 2. Start hybrid session │
│ 3. [Background] Auto-retrain if model >24h old │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────────┴───────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ AUDIO SOURCE │ │ VITALS SOURCE │
│ (Microphone) │ │ (HealthConnect) │
├─────────────────────┤ ├─────────────────────┤
│ Web Audio API │ │ HR, HRV, RR │
│ → Meyda features │ │ → 30s polling │
│ → Breathing analysis│ │ → RMSSD/CV analysis │
└─────────────────────┘ └─────────────────────┘
│ │
└───────────────────┬───────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ HYBRID CLASSIFIER │
│ • Fuses audio + vitals with weighted probabilities │
│ • Applies temporal priors (awake more likely at start/end) │
│ • Two-stage: Awake detection → REM vs NREM │
│ • Computes remConfidence for playback gating │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ REM PLAYBACK GATE │
│ if (stage === 'rem' && remConfidence >= 0.5) → play dream │
│ else → log and skip │
└─────────────────────────────────────────────────────────────────┘
Key Components:
services/sleep.ts- Session management, audio capture, stage transitionsservices/hybridClassifier.ts- Fuses audio + vitals sourcesservices/remOptimizedClassifier.ts- REM-focused classification with learned parametersservices/healthConnect.ts- Android Health Connect integration
// hooks/useLaunchQueue.ts - Shared state via module-level variable
let sharedQueue: Dream[] = [];
const listeners = new Set<() => void>();
export function useLaunchQueue() {
// All components share same queue instance
}- NativeWind (Tailwind) classes for all styling
classNameprop on all components- Courier Prime font family for text (
fontFamily: 'CourierPrime_400Regular')
| Category | Token | Values |
|---|---|---|
| Primary | colors.primary[500] |
#22c55e (Green) |
| Background | darkTheme.background |
#09090b |
| Surface | darkTheme.surface |
#18181b |
| Surface Alt | darkTheme.surfaceAlt |
#27272a |
| Spacing | xs/sm/md/lg/xl |
4/8/16/24/32px |
| Border Radius | sm/md/lg/xl/full |
2/4/8/12/9999px |
-
Color Identity Conflict:
tailwind.config.jsdefines primary as BLUE (#6366f1) buttheme/tokens.tsdefines it as GREEN (#22c55e). Components using NativeWind appear blue, StyleSheet components appear green. -
Hardcoded Background Colors: Many files use hardcoded hex instead of tokens:
#0f0f1a- Should bedarkTheme.backgroundorcolors.gray[950]#1a1a2e- Should bedarkTheme.surface#252542- Should bedarkTheme.surfaceAlt
-
Missing Accessibility Labels: Icon-only buttons in
LaunchQueueCard,DraggableQueueList,SleepHistoryCard, and home screen info button lackaccessibilityLabel.
-
Terminology Inconsistency:
- Use "Browse Dreams" (not "Explore Dreams")
- Use "Start Sleep Tracking" (not "Start Dream Mode")
-
Capitalization Inconsistency: Empty states mix Title Case and Sentence case. Standardize on Sentence case.
-
Icon Style Inconsistency: Trash icon uses both
trashandtrash-outline. Standardize ontrash-outline. -
Modal Close Button: Placement varies (left in
SleepSessionDetailModal, right elsewhere). Standardize on right side. -
Missing 12px Spacing Token: Add
smd: 12to spacing scale. -
Back Button Navigation:
app/dream/[id].tsxusesrouter.replace('/')instead ofrouter.back().
-
Redundant Screen:
app/sleep/index.tsxduplicates functionality ofapp/(tabs)/dream.tsx. -
Hardcoded Font Family: Some files use
'CourierPrime_400Regular'string instead offontFamily.regulartoken.
| Tab | File | Purpose |
|---|---|---|
| Home | app/(tabs)/index.tsx |
Browse dreams by category |
| Search | app/(tabs)/search.tsx |
Full-text dream search |
| Favorites | app/(tabs)/favorites.tsx |
Saved dreams |
| Queue | app/(tabs)/queue.tsx |
Sleep session queue |
| Dream | app/(tabs)/dream.tsx |
Sleep tracking control |
| Settings | app/(tabs)/settings.tsx |
Calibration & debug |
| Modal/Detail | File | Purpose |
|---|---|---|
| Dream Detail | app/dream/[id].tsx |
Dream info and player |
| Dream Launch | app/dream/launch.tsx |
Active dream session |
| About | app/about.tsx |
Instructions |
| Category | Components |
|---|---|
| Base UI | ui/Button, ui/Card, ui/Input, ui/Text, ui/ThemedAlert |
| Dream Display | DreamCard, DreamPlayer, DreamFeed, DreamCardSkeleton |
| Queue Management | LaunchQueueCard, DraggableQueueList, QueueButton |
| Sleep Tracking | SleepModePlayer, SleepStageGraph, SleepHistoryCard, SleepDebugPanel |
| Calibration | VolumeSetup, MicrophoneTest, SensorCalibration |
| Navigation | CategoryFilter, SearchBar |
| Overlays | DarkScreenOverlay, SleepSessionDetailModal |
- Use
as any,@ts-ignore, or@ts-expect-error - Commit without explicit user request
- Suppress TypeScript errors
- Add mock/stub implementations without TODO markers
- Modify visual/styling without delegating to frontend specialist
- Browse/Search with transcript content matching
- Audio playback with progress persistence
- Favorites system (local storage)
- Queue management with shared state
- CI audio generation (Edge TTS + procedural music)
- Health Connect integration (Android) - HR, HRV, respiratory rate, sleep stages
- Hybrid sleep classifier - fuses audio + vitals for stage detection
- Auto-training classifier - learns from user's HealthConnect history
- REM confidence gating - prevents false positive dream playback
- Auth (UI complete, uses mock data)
- Offline indicator (component exists, not integrated)
- HealthKit (iOS) - stub only, Health Connect (Android) is complete
- Supabase backend integration
- Deep link handling
- watchOS/WearOS companion apps
Two-step pipeline in scripts/:
- generate_audio.py → Edge TTS narration →
{id}_full.opus - generate_music.py → Procedural ambient music →
{id}_combined.opus
See scripts/AGENTS.md for detailed audio pipeline documentation.
- Create narrative file:
scripts/narratives/my_dream.txt - Add entry to
scripts/narratives/metadata.json - Rebuild:
python scripts/build_dream_data.py - Generate audio:
python scripts/generate_audio.py --dream dream-N - Commit only
*_combined.opusfiles
// services/remOptimizedClassifier.ts
// Stage 1: Awake Detection
const AWAKE_MEAN_DIFF_BASE_THRESHOLD = 3.0; // HR beat-to-beat variability
const AWAKE_CONSECUTIVE_REQUIRED = 1; // Signals before awake confirmed
// Learned awake priors by 30-min time bins (from Fitbit analysis)
// 0-30min: 38.1%, 30-60min: 4.8%, 60-90min: 1.6%, ...330-360min: 30.9%
// Dynamic threshold adjustment based on time prior:
// prior > 25% → threshold * 0.85 (more sensitive at sleep onset/wake)
// prior < 5% → threshold * 1.3 (less sensitive mid-sleep)
// Stage 2: REM vs NREM
const CV_THRESHOLD = 0.2; // RMSSD coefficient of variation
const REM_CONSECUTIVE_REQUIRED = 2; // Consecutive signals for REM
// Blended awake score: 0.7 * hr_signal + 0.3 * time_prior > 0.4// services/sleep.ts
const MIN_REM_CONFIDENCE_FOR_PLAYBACK = 0.5;
// remConfidence computed from:
// - REM probability from classifier
// - Time bonus (>70 min since sleep start)
// - REM window bonus (in expected REM portion of cycle)
// - Consecutive REM signals
// - REM propensity (increases through night)| Metric | Value |
|---|---|
| REM Sensitivity | 79.8% |
| Awake Sensitivity | 31.0% |
| Awake Precision | 30.8% |
- Web: GitHub Pages at https://context-lab.com/dream-stream/
- CI:
.github/workflows/deploy.ymlgenerates audio, caches based on mockData.ts hash - Audio: Opus format, 64kbps, served from
/audio/dreams/
Android builds are functional as of January 2026. The previous AGP/Material compatibility issues have been resolved.
# Build and run on connected device/emulator
npx expo run:android
# Build APK only
cd android && ./gradlew assembleDebug
# APK output location
android/app/build/outputs/apk/debug/app-debug.apk| Component | Version |
|---|---|
| buildTools | 36.0.0 |
| minSdk | 26 |
| compileSdk | 36 |
| targetSdk | 36 |
| NDK | 27.1.12297006 |
| Kotlin | 2.1.20 |
| Gradle | 8.14.3 |
Health Connect is integrated and working:
- Permissions: Heart rate, respiratory rate, sleep data
- Status shown in Settings > Wearables & Health Connect
- Debug data available in Settings > Health Connect Data Debug
# List available emulators
~/Library/Android/sdk/emulator/emulator -list-avds
# Start emulator
~/Library/Android/sdk/emulator/emulator -avd Medium_Phone_API_35
# Check connected devices
~/Library/Android/sdk/platform-tools/adb devices
# Install APK
~/Library/Android/sdk/platform-tools/adb install -r android/app/build/outputs/apk/debug/app-debug.apk
# Take screenshot
~/Library/Android/sdk/platform-tools/adb exec-out screencap -p > screenshot.png- Project ID:
8d6aab43-b375-4a93-86a2-bb5d4e407871 - Owner:
contextlab
Last updated: 2026-01-15