# Project export: AuraLamp

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: A smart ambient lamp that reads the room’s vibe through visual and audio cues, then sets the soundtrack and lighting to match.
- Devpost: https://devpost.com/software/auralamp
- GitHub: https://github.com/amwang9276/treehacks26.git
- Video: https://www.youtube.com/embed/9urJGGRuCAg?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — angelika (33 commits), Anjali Gorti (16 commits)

## Devpost submission (written by the team)

### Inspiration

We were inspired by the idea of ambient technology that responds quietly and naturally without requiring constant input. AuraLamp explores how shared spaces can adapt to the people in them, automatically adjusting the soundtrack to match the room’s energy.

### What it does

AuraLamp is an ambient lamp that analyzes live audio and visual cues to estimate a room’s energy level. Using microphone input and visual activity detection, the system categorizes the environment into energy states such as calm, focused, or lively. It then selects a song from a curated playlist that best matches the moment. Instead of manually choosing music, AuraLamp responds to the room in real time and sets the tone automatically. How We Built It Front End Built with Next.js + TypeScript for a responsive web interface and fast iteration. Dashboard shows a live camera stream plus categorized runtime logs ([EMOTION], [CONTEXT], [FUSION], [VOICE], [RETRIEVAL], [MUSIC]) to make system behavior transparent. Added a live toggle between Suno and Spotify Retrieval modes so users can switch music strategy without restarting the sensing pipeline. Back End Built with FastAPI + Uvicorn for API routes, orchestration, and runtime control. Implemented Spotify OAuth and session-based auth for account connection. Added runtime endpoints for start/stop, mode switching, logs, and MJPEG camera streaming. Integrated Elasticsearch sync for lyrics at runtime startup (upsert + stale-delete) to keep retrieval data current. CV / FER (Facial Emotion Recognition) Used OpenCV for real-time frame capture and stream handling. Used MediaPipe face detection for practical, low-latency face localization. Used a Hugging Face FER model (e.g., trpakov/vit-face-expression) on CPU for emotion classification. Stabilized emotion triggers over time to avoid noisy emotion flicker causing unnecessary music changes. MER (Music Emotion Retrieval / Matching) Implemented dual music paths: Suno mode: generate instrumental tracks from fused scene context using Suno’s TreeHacks API and begin playback as soon as status reaches streaming. Spotify Retrieval mode: use local song files and rank candidates with MuLan audio-text similarity blended with Elasticsearch lyric relevance. Used MuLan (OpenMuQ/MuQ-MuLan-large) for semantic audio-text alignment and cached embeddings on disk to avoid recomputation. Used Elasticsearch for lyric indexing and semantic text matching, then blended with MuLan scores for final selection. Voice Analysis Captured mic audio using sounddevice. Extract vocal features using librosa library to estimate mood and emotion from pitch, words per minute, volume Extract content of speech using Whisper OpenAI API for transcription Use spaCy NLP to extract key topics and infer the mood of speech from the content Returns an object with information on speech features and content, along with a vector of probable emotions. Context Shot To contextualize the emotions detected from the other input, we also take a “context shot”. We pass in a static image of the room to ChatGPT to describe the location and the occasion. We update this shot relatively infrequently to reduce redundant calls. Sensor Fusion Combined speech features (mood, transcript emotion, keywords/topics) with face and room context in the fusion layer, amplifying and dampening emotions from context with information collected from the vocal features. Pass each of the three outputs of the voice analysis, facial analysis, and context shot to ChatGPT to produce a 5 sentence description of the room’s atmosphere and mood. By continuously updating these inputs, the system selects music that reflects the current atmosphere of the space. Hardware Used ‘Arduino Uno’ to connect mood lighting inputs to an LED array. Created a 3x3 array of RGB LED’s to control light (instead of a color changing bulb, which wasn’t available to us). Used arduino IDE to control lighting inputs for each mood using RGB color setting for each mood. Designed and 3D printed a stylish lamp cover in “onshape” to diffuse the LEDs and make the light more aesthetically pleasing.

### Challenges we ran into

Our biggest challenge was scope. We initially planned to build a more complex adaptive music system, but quickly realized that creating a stable, real-time hardware prototype within 36 hours required focus. We intentionally reduced scope to a curated playlist to ensure reliability and a smooth demo. Integrating hardware inputs with live processing also introduced latency and synchronization issues that required careful debugging. Optimization and parallelization was especially important, as we loaded our models and song data all on CPU. Though transferring to GPU would speed things up, we also optimized our code by doing expensive work only when needed and then reusing results. Voice and retrieval models are lazy-loaded once, MuLan song embeddings and downloaded preview audio are cached on disk to avoid recomputation/network calls, and the server reuses a single embedder and Elasticsearch client instance. It also skips redundant indexing when a user’s index already exists, deduplicates tracks across playlists so each track is embedded once, and ignores repeated identical emotion events to prevent duplicate processing. Since we did not have access to a color changing LED, we had to create a 3 x 3 array of individual red, blue, and green LEDs to mimic this functionality. To make the lighting more uniform, we diffused it using fabric and the 3D printed lamp shade.

### Accomplishments we're proud of

We are proud that we built a functioning hardware-software prototype within a limited timeframe. All four of us are first-time hackers, and this was our first experience integrating live hardware sensing with real-time decision logic. Successfully delivering a working system while adapting our scope was a major milestone for our team.

### What we learned

This project taught us how to turn an ambitious idea into a focused, achievable prototype. We learned how to: Break down abstract concepts like “room energy” into measurable signals Debug hardware-software integration under time pressure Communicate and collaborate effectively across different experience levels Most importantly, we learned that clarity and execution matter more than complexity.

### What's next

Improve the accuracy and nuance of our energy classification system Expand beyond a fixed playlist and incorporate adaptive, personalized music selection Introduce mood-aware responses, such as calming music during stressful moments or ambient sounds during rest Explore subtle support features for studying, social settings, and sleep environments Refine the lamp’s physical design to incorporate a speaker, microphone, and raspberry pi to work remotely without a computer. References Research suggests that music can meaningfully influence anxiety and emotional state, supporting our vision of AuraLamp as a system that responds thoughtfully to how people feel in a space. Sung, H. C., et al. “The Effects of Calming Music Listening Intervention on Anxiety-Related Outcomes in College Nursing Students under Stress.” International Journal of Evidence-Based Healthcare, 2012.

## README (from the GitHub repository)

# treehacks26

Camera + emotion + context/voice fusion + music orchestration.

- `Suno` mode: generate instrumental music from fusion prompt.
- `Spotify Retrieval` mode: select local songs via retrieval (MuLan + Elasticsearch fallback logic).
- Web dashboard streams camera output and categorized runtime logs (`[EMOTION]`, `[FUSION]`, `[MUSIC]`, etc.).

## 1) Root Setup (Windows)

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
```

Linux/macOS:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txt
```

## 2) Environment Variables

Create `.env` in repo root with at least:

```env
OPENAI_API_KEY=...
SUNO_API_KEY=...
SUNO_BASE_URL=https://studio-api.prod.suno.com

SPOTIFY_CLIENT_ID=...
SPOTIFY_CLIENT_SECRET=...
SPOTIFY_REDIRECT_URI=http://127.0.0.1:3000/callback
CLIENT_ORIGIN=http://127.0.0.1:3000
SESSION_SECRET=replace_with_a_long_random_secret
SPOTIFY_SCOPES=playlist-read-private playlist-read-collaborative

ELASTICSEARCH_URL=...
ELASTICSEARCH_API_KEY=...   # or ELASTICSEARCH_USERNAME/ELASTICSEARCH_PASSWORD
ELASTICSEARCH_VERIFY_CERTS=true
ELASTICSEARCH_INDEX=lyrics
MULAN_MODEL_ID=OpenMuQ/MuQ-MuLan-large
```

## 3) Run Web App (Recommended)

### Backend

```powershell
cd server
python -m pip install -r requirements-server.txt
python -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000
```

### Frontend

```powershell
cd client
npm install
```

Create `client/.env.local`:

```env
NEXT_PUBLIC_API_BASE=http://127.0.0.1:8000
```

Then:

```powershell
npm run dev
```

Open `http://127.0.0.1:3000`.

## 4) Dashboard Behavior

- Choose source on `/`:
  - `Suno`
  - `Connect Spotify`
- On `/dashboard`, runtime starts once and keeps camera/emotion/context/voice running.
- Toggle `Suno` / `Spotify Retrieval` on dashboard switches only the music branch (no full runtime restart).
- Dashboard startup also syncs Elasticsearch index `lyrics` from local `lyrics/*.txt` (upsert + stale delete).

## 5) Run Main CLI

```powershell
python main.py --source-type local --index 0
```

Useful flags:

```powershell
python main.py --generate true  --source-type local --index 0 --stable-seconds 1 --suno-poll-interval 2.5
python main.py --generate false --source-type local --index 0 --stable-seconds 1
```

## 6) Lyrics Index Sync (Manual)

To manually sync lyrics files to Elasticsearch:

```powershell
python es_index.py --index-name lyrics --lyrics-dir lyrics
```

This now:
- uploads/updates docs for current lyric files
- deletes stale docs no longer present in local folder

Disable delete-sync:

```powershell
python es_index.py --index-name lyrics --lyrics-dir lyrics --no-sync-delete
```

## 7) Retrieval / MuLan Notes

- Default model: `OpenMuQ/MuQ-MuLan-large`
- Requires `muq` package and compatible `torch` runtime.
- If MuLan fails during retrieval initialization, runtime now falls back to Elastic-only local-song selection instead of hard failing.

## 8) Spotify OAuth Notes

- Spotify app redirect URI must exactly match:
  - `http://127.0.0.1:3000/callback`
- In dev mode, Spotify may restrict accounts unless app/user settings are configured in Spotify Developer Dashboard.



## Detected evidence (automated analysis)

Indexed codebase: 50 recognized source files, 310 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- Hugging Face (technology) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code
- React (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (69 of 69)

```
.gitignore
audio_fetch.py
camera.py
client/.env.local.example
client/next-env.d.ts
client/next.config.js
client/package.json
client/src/app/callback/page.tsx
client/src/app/dashboard/page.tsx
client/src/app/globals.css
client/src/app/layout.tsx
client/src/app/page.tsx
client/src/app/playlists/page.tsx
client/src/app/tracks/[playlistId]/page.tsx
client/src/app/vibe/page.tsx
client/tsconfig.json
client/tsconfig.tsbuildinfo
colorchanging/colorchanging.ino
context_shot.py
docker-compose.elasticsearch.yml
embed.py
es_index.py
facial_emotions.py
fusion.py
lamp_light_control.py
LICENSE
lyrics/dancing_with_your_ghost_sasha.txt
lyrics/happy_pharraell.txt
lyrics/ladyfingers_herb.txt
lyrics/party_rock_anthem_lmfao.txt
lyrics/perfect_ed.txt
lyrics/riptide_vance.txt
lyrics/useless_sacrifice_death.txt
main.py
mulan.py
play_music.py
pyproject.toml
README.md
requirements.txt
retrieval_cases.json
run_mulan.py
server/.env.example
server/app/__init__.py
server/app/config.py
server/app/dashboard_runtime.py
server/app/main.py
server/app/routers/__init__.py
server/app/routers/auth.py
server/app/routers/camera.py
server/app/routers/dashboard_runtime.py
server/app/routers/semantic.py
server/app/routers/spotify.py
server/app/semantic_service.py
server/app/session_store.py
server/app/spotify_api.py
server/app/spotify_oauth.py
server/requirements-server.txt
server/test_lamp.py
server/tests/test_oauth_and_session.py
server/tests/test_routes_mocked.py
spotify_ingest.py
suno_gen_music.py
tests/live_vocal_test.py
tests/test_es_index.py
tests/test_fusion.py
tests/test_mulan_retrieval.py
tests/test_spotify_ingest.py
tests/test_vocal_tracking.py
voice.py
```

### Dependencies

- client/package.json: @types/node@^22.9.0, @types/react@^18.3.12, @types/react-dom@^18.3.1, next@^15.0.0, react@^18.3.1, react-dom@^18.3.1, typescript@^5.6.3
- pyproject.toml: elasticsearch@>=8.14,<9.0, librosa@>=0.10,<1.0, mediapipe@>=0.10.14,<0.11, muq, numpy@>=1.26,<3.0, openai@>=1.40,<2.0, opencv-python@>=4.9,<5.0, Pillow@>=10.0,<12.0, torch@>=2.2,<3.0, transformers@>=4.41,<5.0
- requirements.txt: elasticsearch@>=8.14,<9.0, fastapi@>=0.115,<1.0, librosa@>=0.10.2,<1.0, llvmlite@>=0.40,<0.47, matplotlib@>=3.8,<4.0, mediapipe@>=0.10.14,<0.11, muq, numba@>=0.58,<0.64, numpy@>=1.26,<3.0, openai@>=1.40,<2.0, opencv-python@>=4.9,<5.0, Pillow@>=10.0,<12.0, pydantic@>=2.0,<3.0, pyserial@>=3.5,<4.0, python-dotenv@>=1.0,<2.0, regex@>=2023.10.0,<2027.0.0, scipy@>=1.11,<2.0, sounddevice@>=0.4.6,<1.0, spacy@>=3.7,<4.0, torch@>=2.2,<3.0, transformers@>=4.41,<5.0, uvicorn@>=0.30,<1.0

### Recent commits (newest first)

- Merge pull request #12 from amwang9276/ui
- format dashboard to be more organized
- Merge pull request #11 from amwang9276/arduino
- Support lamp changing
- Merge branch 'ui' of https://github.com/amwang9276/treehacks26 into ui
- update landing page with new product name
- Merge branch 'ui' of https://github.com/amwang9276/treehacks26 into ui
- Update time out for context pic
- Merge pull request #10 from amwang9276/ui
- hania's arduino control code
- speed up initialization of spotify option by lazily loading in some of the embedding on model parameters/data
- Merge branch 'ui' of https://github.com/amwang9276/treehacks26 into ui
- optimize/speed up suno/spotofy switch
- Merge branch 'ui' of https://github.com/amwang9276/treehacks26 into ui
- Updates to context
- fix local song option
- upload new songs + add new songs + loop saved song sections
- updated requirements for repo
- update ui for suno
- add lvie video feed + emotion detection to dahsboard + outputs from the various models

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
numpy>=1.26,<3.0
opencv-python>=4.9,<5.0
mediapipe>=0.10.14,<0.11
torch>=2.2,<3.0
transformers>=4.41,<5.0
Pillow>=10.0,<12.0
openai>=1.40,<2.0
librosa>=0.10.2,<1.0
elasticsearch>=8.14,<9.0
muq
sounddevice>=0.4.6,<1.0
spacy>=3.7,<4.0
scipy>=1.11,<2.0
regex>=2023.10.0,<2027.0.0
matplotlib>=3.8,<4.0
numba>=0.58,<0.64
llvmlite>=0.40,<0.47
pyserial>=3.5,<4.0
fastapi>=0.115,<1.0
uvicorn>=0.30,<1.0
python-dotenv>=1.0,<2.0
pydantic>=2.0,<3.0

```

### pyproject.toml

```
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "treehacks26"
version = "0.1.0"
description = "Modular camera streaming utility for CV prototyping"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
  "numpy>=1.26,<3.0",
  "opencv-python>=4.9,<5.0",
  "mediapipe>=0.10.14,<0.11",
  "torch>=2.2,<3.0",
  "transformers>=4.41,<5.0",
  "Pillow>=10.0,<12.0",
  "openai>=1.40,<2.0",
  "librosa>=0.10,<1.0",
  "elasticsearch>=8.14,<9.0",
  "muq",
]

[tool.setuptools]
py-modules = [
  "camera",
  "emotions",
  "suno",
  "music",
  "main",
  "embed",
  "spotify_ingest",
  "audio_fetch",
  "mulan_embed",
  "es_index",
]

```

### client/package.json

```
{
  "name": "treehacks26-client",
  "private": true,
  "version": "0.1.0",
  "scripts": {
    "dev": "next dev -p 3000",
    "build": "next build",
    "start": "next start -p 3000"
  },
  "dependencies": {
    "next": "^15.0.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "typescript": "^5.6.3",
    "@types/node": "^22.9.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1"
  }
}

```

### main.py

```python
from __future__ import annotations

import argparse
import hashlib
import os
import queue
import re
import sys
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union

import cv2
import serial
from openai import OpenAI
from elasticsearch import Elasticsearch

from camera import FramePacket, build_local_camera_help_text, build_source_from_args
from context_shot import ContextShot
from facial_emotions import EmotionObservation, EmotionProcessor
from fusion import SensorFusion, SensorState
from play_music import MusicPlaybackError, MusicPlayer
from mulan import DEFAULT_MULAN_MODEL_ID, MuLanEmbedError, MuLanEmbedder
from suno_gen_music import SunoError, SunoGenerationResult, generate_from_prompt
from voice import VoiceObservation, VoiceProcessor


DEFAULT_OPENAI_MODEL = "gpt-4.1-mini"
DEFAULT_OPENAI_MAX_TOKENS = 48

# Emotion-to-mood index mapping (matches colorchanging.ino)
EMOTION_TO_MOOD: Dict[str, int] = {
    "focus": 0,
    "sad": 1,
    "calm": 2,
    "happy": 3,
    "angry": 4,
    "romantic": 5,
}


def _parse_color_emotion(text: str) -> Optional[str]:
    """Extract the emotion word after 'COLOR:' in fusion output."""
    match = re.search(r"COLOR:\s*(\w+)", text, re.IGNORECASE)
    if match:
        return match.group(1).strip().lower()
    return None


def _send_lamp_mood(ser: serial.Serial, emotion: str) -> None:
    """Send the mood index to the Arduino lamp over serial."""
    mood_index = EMOTION_TO_MOOD.get(emotion)
    if mood_index is None:
        print(f"[LAMP] unknown emotion '{emotion}', skipping lamp update")
        return
    message = f"{mood_index}\n"
    ser.write(message.encode("utf-8"))
    ser.flush()
    print(f"[LAMP] set mood={mood_index} ({emotion})")


def _read_env_file(path: Path = Path(".env")) -> Dict[str, str]:
    values: Dict[str, str] = {}
    if not path.exists():
        return values
    for line in path.read_text(encoding="utf-8").splitlines():
        stripped = line.strip()
        if not stripped or stripped.startswith("#") or "=" not in stripped:
            continue
        key, value = stripped.split("=", 1)
        values[key.strip()] = value.strip().strip("'").strip('"')
    return values


def get_openai_api_key(explicit: Optional[str] = None) -> Optional[str]:
    if explicit:
        return explicit
    env_key = os.environ.get("OPENAI_API_KEY")
    if env_key:
        return env_key
    file_key = _read_env_file().get("OPENAI_API_KEY")
    if file_key:
        return file_key
    return None


def generate_suno_prompt_for_emotion(
    emotion: str,
    *,
    client: OpenAI,
    model: str = DEFAULT_OPENAI_MODEL,
    max_tokens: int = DEFAULT_OPENAI_MAX_TOKENS,
    timeout_s: float = 30.0,
    context: Optional[str] = None,
) -> str:
    effective_client = client.with_options(timeout=timeout_s)
    print(f"[OPENAI] generating prompt for emotion '{emotion}'")
    user_content = f"Detected emotion: {emotion}"
    if context:
        user_content += f"\nRoom context: {context}"
    try:
        completion = effective_client.chat.completions.create(
            model=model,
            messages=[
                {
                    "role": "system",
                    "content": (
                        "Output exactly one short Suno music prompt line. "
                        "No markdown, no quotes."
                    ),
                },
                {"role": "user", "content": user_content},
            ],
            max_tokens=max(16, max_tokens),
            temperature=0.5,
        )
    except Exception as err:
        raise RuntimeError(f"OpenAI SDK error: {err}") from err

    content = completion.choices[0].message.content if completion.choices else None
    text = (content or "").strip()
    if not text:
        raise RuntimeError("OpenAI completion did not include prompt text.")
    return text


@dataclass
class StableEmotionChangeDetector:
    min_stable_seconds: float = 1.0
    candidate_emotion: Optional[str] = None
    candidate_since_s: Optional[float] = None
    last_triggered_emotion: Optional[str] = None

    def observe(self, observation: EmotionObservation) -> Optional[str]:
        emotion = observation.label
        now = observation.timestamp_s
        if not emotion:
            self.candidate_emotion = None
            self.candidate_since_s = None
            return None

        if emotion != self.candidate_emotion:
            self.candidate_emotion = emotion
            self.candidate_since_s = now
            return None

        if self.candidate_since_s is None:
            self.candidate_since_s = now
            return None

        if now - self.candidate_since_s < self.min_stable_seconds:
            return None

        if emotion == self.last_triggered_emotion:
            return None

        self.last_triggered_emotion = emotion
        return emotion


def _choose_track_url(result: SunoGenerationResult) -> Optional[str]:
    for track in result.tracks:
        if track.stream_url:
            return track.stream_url
        if track.audio_url:
            return track.audio_url
    return None


# Queue items: either "__STOP__" or (emotion, context_or_none)
_MusicQueueItem = Union[str, tuple]


@dataclass
class LocalSongEmbedding:
    key: str
    path: Path
    vector: "np.ndarray"


def _parse_bool_arg(value: str) -> bool:
    lowered = (value or "").strip().lower()
    if lowered in {"1", "true", "yes", "y"}:
        return True
    if lowered in {"0", "false", "no", "n"}:
        return False
    raise argparse.ArgumentTypeError(
        "Expected boolean value for --generate (true/false)."
    )


def _resolve_setting(name: str, default: str) -> str:
    value = os.environ.get(name)
    if value:
        return value
    file_value = _read_env_file().get(name)
    if file_value:
        return file_value
    return default


def _resolve_bool_setting(name: str, default: bool) -> bool:
    
[truncated — 31942 more characters]
```

### server/app/main.py

```python
from __future__ import annotations

import logging

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse

from .config import ConfigError, load_settings
from .dashboard_runtime import DashboardRuntime
from .routers.auth import router as auth_router
from .routers.camera import router as camera_router
from .routers.dashboard_runtime import router as dashboard_runtime_router
from .routers.semantic import router as semantic_router
from .routers.spotify import router as spotify_router
from .semantic_service import SemanticService
from .session_store import InMemorySessionStore, SessionCookieSigner


def create_app() -> FastAPI:
    app = FastAPI(title="treehacks26 server", version="0.1.0")

    try:
        settings = load_settings()
    except ConfigError as err:
        # App still starts, but surfaces a clear startup configuration error.
        @app.get("/healthz")
        def _healthz_failed() -> JSONResponse:
            return JSONResponse(
                status_code=500,
                content={"error": {"code": "CONFIG_ERROR", "message": str(err)}},
            )

        return app

    app.state.settings = settings
    app.state.session_store = InMemorySessionStore()
    app.state.cookie_signer = SessionCookieSigner(settings.session_secret)
    app.state.semantic_service = SemanticService(settings, app.state.session_store)
    app.state.dashboard_runtime = DashboardRuntime(settings)
    logging.basicConfig(
        level=logging.INFO if settings.spotify_debug else logging.WARNING,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )

    app.add_middleware(
        CORSMiddleware,
        allow_origins=list(settings.client_origins) or [settings.client_origin],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    @app.get("/healthz")
    def healthz() -> dict:
        return {"ok": True}

    app.include_router(auth_router)
    app.include_router(camera_router)
    app.include_router(dashboard_runtime_router)
    app.include_router(spotify_router)
    app.include_router(semantic_router)
    return app


app = create_app()

```

### client/src/app/layout.tsx

```typescript
import "./globals.css";
import type { ReactNode } from "react";

export const metadata = {
  title: "treehacks26 client",
  description: "Spotify connect and playlist browser",
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### client/src/app/page.tsx

```typescript
"use client";

import Link from "next/link";
import { loginUrl } from "../lib/api";

export default function HomePage() {
  return (
    <main className="container">
      <div className="card">
        <h1>Welcome to AuraLamp!</h1>
        <p className="muted">
          Select whether to use AI-generated music powered by Suno or connect Spotify to continue.
        </p>
      </div>

      <div className="source-grid">
        <div className="card">
          <h2>Feeling Adventurous</h2>
          <p className="muted">
            Generate new music dynamically from detected room mood and context.
          </p>
          <Link className="btn" href="/dashboard?mode=suno">
            Use AI Generated Music
          </Link>
        </div>

        <div className="card">
          <h2>The Classics</h2>
          <p className="muted">
            Connect your Spotify account so the app can access your authorized music data and you can enjoy your favorite tracks.
          </p>
          <a className="btn" href={loginUrl()}>
            Connect Spotify
          </a>
        </div>
      </div>
    </main>
  );
}

```

### client/src/app/callback/page.tsx

```typescript
"use client";

import { useEffect } from "react";
import { useSearchParams } from "next/navigation";

function apiBase(): string {
  return process.env.NEXT_PUBLIC_API_BASE ?? "http://127.0.0.1:8000";
}

export default function CallbackPage() {
  const params = useSearchParams();

  useEffect(() => {
    const qs = params.toString();
    const target = `${apiBase()}/auth/callback${qs ? `?${qs}` : ""}`;
    window.location.href = target;
  }, [params]);

  return (
    <main className="container">
      <div className="card">
        <h2>Completing Spotify login...</h2>
        <p className="muted">Redirecting to backend callback.</p>
      </div>
    </main>
  );
}

```

### client/src/app/vibe/page.tsx

```typescript
"use client";

import Link from "next/link";
import { useEffect, useState } from "react";

import { getSemanticIndexStatus, searchVibe, type SemanticIndexStatus, type VibeSearchItem } from "../../lib/api";

export default function VibePage() {
  const [text, setText] = useState("");
  const [topK, setTopK] = useState(10);
  const [status, setStatus] = useState<SemanticIndexStatus | null>(null);
  const [results, setResults] = useState<VibeSearchItem[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let mounted = true;
    async function loadStatus() {
      try {
        const s = await getSemanticIndexStatus();
        if (!mounted) return;
        setStatus(s);
      } catch (err) {
        if (!mounted) return;
        setError(err instanceof Error ? err.message : "Failed to load index status");
      }
    }
    void loadStatus();
    return () => {
      mounted = false;
    };
  }, []);

  const ready = status?.status === "completed" && (status?.indexed || 0) > 0;

  return (
    <main className="container">
      <div className="card">
        <Link href="/playlists" className="btn">
          Back to playlists
        </Link>
      </div>
      <div className="card">
        <h1>Vibe Search</h1>
        <p className="muted">
          Describe the room mood and find semantically similar tracks from indexed Spotify previews.
        </p>
        <p className="muted">
          Index status: {status?.status ?? "unknown"} | Indexed tracks: {status?.indexed ?? 0}
        </p>
        {!ready && (
          <p className="muted">
            Index is not ready yet. Return to playlists and wait for indexing to complete.
          </p>
        )}
        <div style={{ display: "grid", gap: 12 }}>
          <input
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder="e.g. calm rainy evening study vibe"
            style={{ padding: 10, borderRadius: 8, border: "1px solid #d0d5dd" }}
          />
          <input
            type="number"
            value={topK}
            min={1}
            max={50}
            onChange={(e) => setTopK(Math.max(1, Math.min(50, Number(e.target.value) || 10)))}
            style={{ padding: 10, borderRadius: 8, border: "1px solid #d0d5dd", width: 120 }}
          />
          <button
            className="btn"
            disabled={!ready || !text.trim() || loading}
            onClick={async () => {
              setLoading(true);
              setError(null);
              try {
                const resp = await searchVibe(text.trim(), topK);
                setResults(resp.items);
              } catch (err) {
                setError(err instanceof Error ? err.message : "Search failed");
              } finally {
                setLoading(false);
              }
            }}
          >
            {loading ? "Searching..." : "Search vibe"}
          </button>
        </div>
      </div>

      {error && (
        <div className="card">
          <p className="muted">{error}</p>
        </div>
      )}

      {!loading && !error && results.length === 0 && ready && (
        <div className="card">
          <p className="muted">No results yet. Try a different vibe description.</p>
        </div>
      )}

      {!loading &&
        !error &&
        results.map((item) => (
          <div key={`${item.track_id}-${item.name}`} className="card">
            <h3>{item.name}</h3>
            <p className="muted">
              {(item.artists || []).join(", ")} | Album: {item.album || "Unknown"} | Score:{" "}
              {item.score.toFixed(4)}
            </p>
            <p className="muted">Playlists: {(item.playlist_names || []).join(", ") || "n/a"}</p>
            <p className="muted">Preview available: {item.preview_url ? "Yes" : "No"}</p>
            {item.spotify_url ? (
              <a className="btn" href={item.spotify_url} target="_blank" rel="noreferrer">
                Open in Spotify
              </a>
            ) : null}
          </div>
        ))}
    </main>
  );
}

```

### client/src/app/dashboard/page.tsx

```typescript
"use client";

import { useEffect, useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";

function apiBase(): string {
  return process.env.NEXT_PUBLIC_API_BASE ?? "http://127.0.0.1:8000";
}

const LOG_COLUMNS = ["EMOTION", "CONTEXT", "VOICE", "FUSION", "RETRIEVAL", "MUSIC"];

type RuntimeLogs = {
  running: boolean;
  [tag: string]: string[] | boolean;
};

export default function DashboardPage() {
  const router = useRouter();
  const params = useSearchParams();
  const [logs, setLogs] = useState<RuntimeLogs>({ running: false });
  const [error, setError] = useState<string | null>(null);
  const [mode, setMode] = useState<"suno" | "spotify">(
    params.get("mode") === "spotify" ? "spotify" : "suno"
  );
  const generate = mode === "suno";

  useEffect(() => {
    if (params.get("spotify_connected") === "1") {
      window.alert("Spotify successfully connected");
    }
  }, [params]);

  useEffect(() => {
    const qp = params.get("mode");
    if (qp === "spotify" || qp === "suno") {
      setMode(qp);
    }
  }, [params]);

  useEffect(() => {
    let cancelled = false;
    const start = async () => {
      try {
        await fetch(`${apiBase()}/dashboard/runtime/start`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ generate }),
          credentials: "include",
        });
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : "Failed to start runtime");
        }
      }
    };
    start();
    return () => {
      cancelled = true;
      fetch(`${apiBase()}/dashboard/runtime/stop`, {
        method: "POST",
        credentials: "include",
      }).catch(() => {
        // noop
      });
    };
  }, []);

  useEffect(() => {
    let cancelled = false;
    const applyMode = async () => {
      try {
        await fetch(`${apiBase()}/dashboard/runtime/mode`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ generate }),
          credentials: "include",
        });
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : "Failed to switch mode");
        }
      }
    };
    applyMode();
    return () => {
      cancelled = true;
    };
  }, [generate]);

  useEffect(() => {
    let cancelled = false;
    const tick = async () => {
      try {
        const res = await fetch(`${apiBase()}/dashboard/runtime/logs`, {
          cache: "no-store",
          credentials: "include",
        });
        if (!res.ok) {
          throw new Error(`status ${res.status}`);
        }
        const payload = (await res.json()) as RuntimeLogs;
        if (!cancelled) {
          setLogs(payload);
          setError(null);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : "Failed to load logs");
        }
      }
    };
    tick();
    const id = setInterval(tick, 1000);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
  }, []);

  const columns = useMemo(
    () =>
      LOG_COLUMNS.map((tag) => ({
        tag,
        entries: Array.isArray(logs[tag]) ? (logs[tag] as string[]) : [],
      })),
    [logs]
  );

  return (
    <main className="container">
      <section className="dashboard-layout">
        <div className="card camera-card">
          <h1>Dashboard</h1>
          <div className="toggle-row">
            <button
              className={`btn toggle-btn ${mode === "suno" ? "active" : ""}`}
              onClick={() => {
                setMode("suno");
                router.replace("/dashboard?mode=suno");
              }}
              type="button"
            >
              Ai-Gen
            </button>
            <button
              className={`btn toggle-btn ${mode === "spotify" ? "active" : ""}`}
              onClick={() => {
                setMode("spotify");
                router.replace("/dashboard?mode=spotify");
              }}
              type="button"
            >
              Spotify Retrieval
            </button>
          </div>
          <p className="muted">
            Runtime status: {logs.running ? "running" : "stopped"} | mode:{" "}
            {generate ? "suno" : "spotify retrieval"}
          </p>
          {error ? <p style={{ color: "#b42318" }}>Error: {error}</p> : null}
          <div className="camera-frame-wrap">
            <img
              src={`${apiBase()}/dashboard/runtime/stream?fps=15`}
              alt="Runtime camera stream"
              className="camera-frame"
            />
          </div>
        </div>
        <section className="log-grid">
          {columns.map((column) => (
            <div key={column.tag} className="card">
              <h3>[{column.tag}]</h3>
              <div className="log-list">
                {column.entries.length === 0 ? (
                  <p className="muted">No output yet.</p>
                ) : (
                  column.entries.map((line, idx) => (
                    <p key={`${column.tag}-${idx}`} className="log-line">
                      {line}
                    </p>
                  ))
                )}
              </div>
            </div>
          ))}
        </section>
      </section>
    </main>
  );
}

```

[42 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]