# Project export: ThirdParty

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: An always-on relationship journal that flags stress moments and patterns in your relationships, then guides your reflection so you stay in sync.
- Devpost: https://devpost.com/software/thirdparty
- GitHub: https://github.com/kevenpi/Third-Party
- Demo: https://thirdparty-gamma.vercel.app/timeline
- Video: https://www.youtube.com/embed/9p4Y_lVemHo?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — Cursor (25 commits), Tane Kim (25 commits), Arthur Ilyasov (20 commits), kevenpi (12 commits), Claude Opus 4.5 (10 commits)

## Devpost submission (written by the team)

### Inspiration

TreeHacks fell on Valentine's Day. One of our teammates spent the entire week fighting with his girlfriend about it. The same argument kept resurfacing in different forms. Over text, at dinner, in the car. By Friday he couldn't even remember what they were actually fighting about. Just a heavy foreboding feeling and the sense that it was going to happen again. That's the problem. People fight and can't even pinpoint why they're fighting. You can't fix a pattern you can't see. And you can't see it because you're always inside the conversation, never above it. There's a gap between what you felt and what actually happened. We built Third Party to fill that gap.

### What it does

ThirdParty runs in the background on your phone or Meta glasses, capturing conversations and turning your day into a private relationship timeline. It transcribes and breaks interactions into moments, tags tone shifts and flags important points by aligning them with wearable stress proxy spikes. It also saves who you were with using facial recognition that links moments to contacts, so you can see patterns per relationship, not just per day. Why always-on recording? We know always-on recording sounds uncomfortable. But every generation resists the next layer of self-awareness, then can't live without it. Your phone already tracks every step you take and every place you go, but the most important part of your life, your relationships, has zero data. The next generation won't think twice about having a conversation archive the same way nobody thinks twice about a photo library today. We're just building it first.

### How we built it

Always-on relationship capture is sensitive, so the biggest challenge was designing for trust: clear consent, minimizing what gets shared, and keeping the “shared session” partner-safe. We also had to handle noisy signals, because wearable stress proxies spike for lots of reasons. On the technical side, segmenting messy real conversations into moments that feel accurate, then keeping the journaling prompts calm and nonjudgmental, took a lot of iteration.

### Challenges we ran into

Always-on capture is sensitive, so we focused on trust: consent, minimizing what gets shared, and keeping shared sessions partner-safe. Signals are also noisy, so we decided that stress spikes have to treated as places to pay attention to and not “this caused that.” Our biggest technical blocker was the people tab. It took a while to get facial recognition and speaker diarization working together reliably so moments attach to the right person, especially with imperfect audio, interruptions, and overlapping speech.

### Accomplishments we're proud of

Continuous audio capture + image recognition + live conversation parsing that structures your day automatically, no manual journaling required. A visual, scrollable map of your day where each interaction becomes a “relational bubble.” Bubble size & temperature reflect conversation duration, discourse intensity (mock cortisol / heart rate signals), AI-scored meaningfulness, and emotional energy inferred from tone Every person has a living profile with a full archive of past conversations, typical topics & emotional patterns, interaction frequency & tone trends, editable labels, and custom photos/notes. Simulated physiological overlays (cortisol/heart-rate proxies) demonstrate how real biometric hardware could quantify escalation and calm over time. A UI that feels quiet and human, not clinical

### What we learned

Most people do not need a verdict after a conflict. They need structure. When you can slow down, separate facts from stories, and name the underlying need, repair gets easier. We also learned that patterns matter more than single arguments: one tense moment feels random, but repeated triggers become actionable. Finally, for something this personal, trust is not a feature, it is the product.

### What's next

The system automatically identifies pivotal moments: laughter spikes, escalation shifts, reconciliations, deep exchanges, and surfaces them as highlights. Two users can merge perspectives into a partner-safe shared summary that removes adversarial framing and highlights mutual ground. Image recognition adds environmental metadata (location, setting, group context) to conversation logs, giving relational context, not just transcripts.

## README (from the GitHub repository)

# ThirdParty

ThirdParty is a relationship mirror and guided journaling app for TreeHacks 2026.

## Stack

- Next.js 14 App Router
- TypeScript
- Anthropic SDK + strict JSON validation with zod
- Local JSON storage in `data/`

## Quick start

1. **Install dependencies**

   ```bash
   npm install
   ```

2. **Environment**

   ```bash
   cp .env.example .env.local
   ```

   Edit `.env.local` and set at least:

   - `OPENAI_API_KEY=sk-...` (required for Voice tab transcription + diarization)
   - `ANTHROPIC_API_KEY=...` (for mediator/reflections; optional)

   Never commit `.env.local` or paste keys into the repo.

3. **Run the app**

   ```bash
   npm run dev
   ```

   Open [http://localhost:3003](http://localhost:3003). Use the **Voice** tab to upload or record and transcribe with speaker labels.

4. **Optional: real speaker IDs (ECAPA-TDNN)**

   For persistent “who is this voice?” across sessions (not just placeholder labels):

   - Install Python 3 and pip, then run the embedder in a **second terminal**:

     ```bash
     npm run embedder
     ```

     Or manually:

     ```bash
     cd services/speaker_embedder
     pip install -r requirements.txt
     uvicorn app:app --host 0.0.0.0 --port 5000
     ```

   - In `.env.local` add (or uncomment):

     ```
     SPEAKER_EMBEDDER_URL=http://localhost:5000/embed
     ```

   - Restart `npm run dev`. The Voice page will show “ECAPA-TDNN” when the embedder is reachable.

5. **Optional: audio conversion (webm/mp3 → WAV)**

   The app uses **ffmpeg-static** (installed with `npm install`) to convert uploads to 16 kHz mono WAV for best embedder results. No separate ffmpeg install needed. If conversion fails (e.g. unsupported format), transcription still runs; speaker IDs may be less accurate without the embedder.

## Run locally (summary)

Same as Quick start above: `npm install` → copy `.env.example` to `.env.local` and set keys → `npm run dev` → optional `npm run embedder` + `SPEAKER_EMBEDDER_URL`.

## Conversation awareness and Meta glasses

The app now includes a conversation-awareness detector and recording pipeline:

- `POST /api/conversationAwareness/listen`:
  - body: `{ "listeningEnabled": true | false }`
- `GET /api/conversationAwareness/state`:
  - returns detector state, recent sessions, and recent events
- `POST /api/conversationAwareness/ingestSignal`:
  - body: `{ "source": "microphone" | "meta_glasses" | "phone_camera", "audioLevel": 0..1, "presenceScore": 0..1, "speakerHints": [{ personTag, speakingScore }] }`
- `POST /api/conversationAwareness/uploadClip`:
  - body: `{ "sessionId": "...", "audioBase64": "...", "mimeType": "audio/webm" }`
- `POST /api/metaGlasses/ingest`:
  - body: `{ "deviceId": "...", "audioLevel": 0..1, "speakerHints": [{ personTag, speakingScore }] }`

### Safety behavior

- Facial recognition is not implemented.
- Identity is based on consented person tags and speaker hints only.
- Raw captured audio is stored locally in `data/awareness/clips` and is not shared by the shared-session flow.
- Phone camera mode computes co-presence and motion scores only. It does not identify people and does not persist video frames.

## UI flow

- Go to `/timeline`
- Tap the gear icon to open `/settings`
- Start listening to activate microphone monitoring, optional phone camera co-presence monitoring, and detector-triggered recording
- Use the Meta glasses signal panel to ingest device-side speaker hints

## Voice: Transcribe + Speaker Identification

Two pipelines:

1. **OpenAI + speaker memory (recommended)**  
   **OpenAI `gpt-4o-transcribe-diarize`** for transcription + diarization (speaker turns). Then **speaker embeddings + clustering** (cosine similarity, centroid updates) to build persistent “who is this voice?” across sessions. No Azure Speaker Recognition; open-world discovery. See [docs/voice-pipeline.md](docs/voice-pipeline.md).

2. **Pyannote diarization + speaker memory (optional)**  
   Run local pyannote diarization service and set:
   - `VOICE_DIARIZATION_BACKEND=pyannote`
   - `PYANNOTE_DIARIZER_URL=http://localhost:5010/diarize`
   This uses pyannote for speaker-turn detection and keeps the same speaker clustering/persistent profiles pipeline.

3. **Google + Azure (optional)**  
   Google Speech-to-Text for diarization; Azure Speaker Recognition to identify **enrolled** speakers only.

### Setup (OpenAI pipeline)

See **Quick start** above. In short:

1. Set **OPENAI_API_KEY** in `.env.local` (never commit it).
2. **Real speaker IDs (optional):** Run `npm run embedder` in a second terminal (or run the Python service manually; see Quick start). Set **SPEAKER_EMBEDDER_URL=http://localhost:5000/embed** in `.env.local`.  
   Details: [docs/speaker-embedding-analysis.md](docs/speaker-embedding-analysis.md).
3. **Audio conversion:** The app uses **ffmpeg-static** (installed with npm) to convert uploads to WAV 16 kHz mono; no separate ffmpeg install needed.

### Setup (Pyannote diarization backend)

1. In a second terminal run:
   - `npm run diarizer`
   - or follow [`services/pyannote/README.md`](services/pyannote/README.md)
2. In `.env.local` set:
   - `VOICE_DIARIZATION_BACKEND=pyannote`
   - `PYANNOTE_DIARIZER_URL=http://localhost:5010/diarize`
3. Keep `OPENAI_API_KEY` optional for fallback behavior if the local pyannote service is unavailable.

### Setup (Google + Azure)

1. **Google Cloud**
   - Create a project and enable the [Speech-to-Text API](https://console.cloud.google.com/apis/library/speech.googleapis.com).
   - Create a service account, download a JSON key, and set in `.env.local`:
     - `GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/your-key.json`
   - Or use `gcloud auth application-default login` and set `GOOGLE_CLOUD_PROJECT=your-project-id`.

2. **Azure**
   - Create a [Speech resource](https://portal.azure.com) and in `.env.local` set:
     - `AZURE_SPEECH_KEY=your-key`
     - `AZURE_SPEECH_REGION=westus` (or your region).

3. Copy [.env.example](.env.example) to `.env.local` and fill in the keys.

### Flow

- **Voice tab**: Choose “OpenAI + speaker memory” (default) or “Google + Azure”. Upload or record → “Transcribe & identify”. With OpenAI: segments get stable speaker IDs over time; you can name speakers via `PATCH /api/voice/speakers`. With Google+Azure: enroll people in People → person → “Enroll voice”, then transcribe to match to those enrolled.

## About

TreeHacks 2026 project


## Detected evidence (automated analysis)

Indexed codebase: 103 recognized source files, 572 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (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
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 149)

```
.env.example
.gitignore
App.tsx
app/agent/page.tsx
app/api/agent/chat/route.ts
app/api/analyzeDay/route.ts
app/api/biometrics/analyze/route.ts
app/api/biometrics/ingest/route.ts
app/api/conversationAwareness/ingestSignal/route.ts
app/api/conversationAwareness/listen/route.ts
app/api/conversationAwareness/state/route.ts
app/api/conversationAwareness/uploadClip/route.ts
app/api/day/route.ts
app/api/face/enroll/route.ts
app/api/face/identify/route.ts
app/api/face/people/route.ts
app/api/generateSession/route.ts
app/api/markMoment/route.ts
app/api/metaGlasses/ingest/route.ts
app/api/people/[id]/avatar/route.ts
app/api/people/[id]/route.ts
app/api/people/route.ts
app/api/review/route.ts
app/api/sessionContext/route.ts
app/api/timeline/conversation/route.ts
app/api/timeline/route.ts
app/api/voice/conversations/by-speaker/route.ts
app/api/voice/embedder-status/route.ts
app/api/voice/enroll/route.ts
app/api/voice/identify/route.ts
app/api/voice/ingest/route.ts
app/api/voice/process/route.ts
app/api/voice/processOpenAI/route.ts
app/api/voice/speakers/route.ts
app/api/voice/transcribe/route.ts
app/conversation/[id]/page.tsx
app/glaze/[id]/page.tsx
app/globals.css
app/layout.tsx
app/page.tsx
app/people/[id]/page.tsx
app/people/page.tsx
app/review/[date]/page.tsx
app/session/page.tsx
app/settings/page.tsx
app/tags/page.tsx
app/timeline/page.tsx
app/voice/page.tsx
components/Badge.tsx
components/BiometricChart.tsx
components/BottomNav.tsx
components/Card.tsx
components/Chip.tsx
components/ConversationListener.tsx
components/Dashboard.tsx
components/Mediator.tsx
components/MessageCorrelationCard.tsx
components/Nav.tsx
components/Settings.tsx
components/SidebarNav.tsx
components/SpikeAnalysisPanel.tsx
components/Stepper.tsx
data/analyzed-days/.gitkeep
data/analyzed-days/2026-02-14.json
data/awareness/.gitkeep
data/awareness/clips/.gitkeep
data/awareness/debugEvents.json
data/awareness/events/.gitkeep
data/awareness/events/2026-02-14.json
data/awareness/events/2026-02-15.json
data/awareness/sessions.json
data/awareness/state.json
data/biometrics.json
data/biometrics/rec_1771122424996_db907811.json
data/biometrics/seed_arthur_1.json
data/biometrics/seed_arthur_2.json
data/biometrics/seed_arthur_3.json
data/biometrics/seed_demo_arthur_1.json
data/biometrics/seed_demo_arthur_2.json
data/biometrics/seed_demo_jessica_1.json
data/biometrics/seed_demo_kevin_1.json
data/biometrics/seed_demo_kevin_2.json
data/biometrics/seed_demo_tane_1.json
data/biometrics/seed_demo_tane_2.json
data/biometrics/seed_kevin_1.json
data/biometrics/seed_kevin_2.json
data/biometrics/seed_kevin_3.json
data/biometrics/seed_tane_1.json
data/reviews/.gitkeep
data/reviews/2026-02-14.json
data/sessions/.gitkeep
data/timeline/2026-02-07.json
data/timeline/2026-02-08.json
data/timeline/2026-02-09.json
data/timeline/2026-02-10.json
data/timeline/2026-02-11.json
data/timeline/2026-02-12.json
data/timeline/2026-02-13.json
data/timeline/2026-02-14.json
docs/speaker-embedding-analysis.md
docs/voice-pipeline.md
index.html
index.tsx
lib/awarenessStorage.ts
lib/biometricRecorder.ts
lib/biometrics.ts
lib/biometricStorage.ts
lib/claudeAgent.ts
lib/conversationAwareness.ts
lib/demoData.ts
lib/faceRecognition.ts
lib/openaiKey.ts
lib/parsers.ts
lib/review.ts
lib/runtimePaths.ts
lib/sample.ts
lib/schemas.ts
lib/storage.ts
lib/timelineStorage.ts
lib/ui.ts
[29 more files omitted for size]
```

### Dependencies

- package.json: @anthropic-ai/sdk@latest, @google-cloud/speech@^7.2.1, @google/genai@^1.41.0, @types/node@^20.12.12, @types/react@^18.2.66, @types/react-dom@^18.2.22, eslint@^8.57.0, eslint-config-next@^14.2.0, ffmpeg-static@^5.2.0, framer-motion@^12.34.0, lucide-react@^0.564.0, next@^14.2.0, openai@^6.22.0, react@^18.2.0, react-dom@^18.2.0, recharts@^3.7.0, typescript@^5.4.5, zod@^3.23.8
- services/pyannote/requirements.txt: fastapi, numpy, pyannote.audio, soundfile, torch, torchaudio, uvicorn
- services/speaker_embedder/requirements.txt: fastapi@>=0.100.0, numpy@>=1.24.0, python-multipart@>=0.0.6, soundfile@>=0.12.0, speechbrain@>=1.0.0, torch@>=2.0.0, torchaudio@>=2.0.0, uvicorn[standard]@>=0.22.0

### Recent commits (newest first)

- no more debug ui
- keven's facial
- idk
- fuck# Please enter the commit message for your changes. Lines starting
- faces
- face
- idek
- preview
- feat: tags browsing page with custom tags and filtered view
- feat: add conversation tag/category system
- Fix bottom nav icons to show bright gold when active
- fix: constrain day selector to max-w-md and clip overflow on sticky header
- fix: mobile scrollable day selector pills
- face
- face
- facial recognition
- Keep today's timeline blank until real conversations are logged.
- Harden conversation detection with start/stop hysteresis and more resilient live transcription handling.
- Restore full upload-clip functionality with classification-gated bubble creation.
- Overwrite 2026-02-13 timeline with Stanford-only sample bubbles.

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

### docs/voice-pipeline.md

```markdown
# Voice pipeline: diarization + speaker clustering

## Architecture (Layer 1–3)

1. **Layer 1 – Diarization (speaker turns)**  
   Default is **OpenAI `gpt-4o-transcribe-diarize`** (transcript + speaker turns).  
   Optional backend is local **pyannote** (`services/pyannote`) for speaker-turn detection.

2. **Layer 2 – Cross-episode speaker identity**  
   We do **not** use Azure Speaker Recognition (retired; and it’s for “which of these enrolled speakers?”, not open-world discovery).  
   Instead:
   - For each diarized speaker segment we compute a **speaker embedding** (voiceprint vector).
   - **Online clustering**: match new embeddings to existing speaker centroids (cosine similarity); if above threshold, assign to that speaker; else create a new speaker.
   - Centroids are updated with an exponential moving average (EMA) as we see more segments.
   - Optional: user can **name** a speaker (e.g. “Roommate”, “Mom”) via `PATCH /api/voice/speakers`.

3. **Layer 3 – Conversation grouping**  
   Chunks can be grouped into conversations by silence gap (`CONVO_GAP_MS`). Right now single-upload flow creates one chunk = one conversation.

## Data model (file-based under `data/voice/`)

- **Chunks**: `data/voice/chunks/{id}.json` (metadata) + `data/voice/chunks/audio/{id}.wav` (audio).
- **Conversations**: `data/voice/conversations/default/{id}.json` with `chunk_ids`.
- **Transcript segments**: `data/voice/segments/{conversationId}_{chunkId}.json` with `speaker_local`, `speaker_global_id`, `start_ms`, `end_ms`, `text`.
- **Speakers**: `data/voice/speakers/default.json` – list of `{ id, user_id, display_name, centroid, last_seen_at }`.

## API

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/voice/ingest` | POST | Upload audio (formData `audio`). Creates chunk + one conversation. Returns `chunkId`, `conversationId`. |
| `/api/voice/processOpenAI` | POST | Run full pipeline. Either JSON `{ conversationId }` or formData `audio`. Returns `segments` (with `speaker_global_id`, `speaker_display_name`), `speakers`, `speakersCreated`. |
| `/api/voice/speakers` | GET | List speakers. PATCH body `{ speakerId, display_name }` to name a speaker. |
| `/api/voice/conversations/by-speaker?speakerId=...` | GET | List conversation IDs that include this speaker (for "all conversations with this person"). |
| `/api/voice/embedder-status` | GET | `{ configured, ok, model? }` — whether embedder is set and reachable (for UI). |

## Env

- **OPENAI_API_KEY** – required for OpenAI diarization backend.
- **VOICE_DIARIZATION_BACKEND** – optional (`pyannote` to prefer pyannote service; default OpenAI).
- **PYANNOTE_DIARIZER_URL** / **PYANNOTE_SERVICE_URL** – optional pyannote endpoint (`/diarize`).
- **SPEAKER_EMBEDDER_URL** – optional. If set, POST audio bytes to this URL; expect `{ embedding: number[] }`. If unset, a placeholder embedding is used so clustering runs for testing.
- **SPEAKER_MATCH_THRESHOLD** – cosine similarity threshold (default `
[truncated — 1195 more characters]
```

### docs/speaker-embedding-analysis.md

```markdown
# Speaker recognition: ECAPA-TDNN vs pyannote — analysis and recommendation

## What you need for “recognize voices, group by profile, store by person”

1. **Diarization** – “Who spoke when?” (segments with labels S0, S1, …).  
   **Already in place:** OpenAI `gpt-4o-transcribe-diarize`.

2. **Speaker embeddings** – A fixed-size vector per voice (voiceprint) so you can compare “is this the same person?” across clips.  
   **Currently:** Placeholder. For real recognition you need a real embedder.

3. **Clustering / profiles** – Match new embeddings to existing speakers (cosine similarity + centroid), create new speaker profiles when no match.  
   **Already in place:** `speakerClustering.ts` + `speakerStorage.ts`.

4. **Storing by person** – Every transcript segment has `speaker_global_id`; list “all conversations where this speaker appeared” and optionally show “all lines by this person.”  
   **Already in place:** segments and speakers in `data/voice/`; one extra API to list conversations by speaker is useful.

So the only missing piece for **full** functionality is a **real speaker embedder** that turns audio into a 192‑dim (or similar) vector. Two main options: **SpeechBrain ECAPA-TDNN** and **pyannote**.

---

## Option A: SpeechBrain ECAPA-TDNN (recommended)

**What it is**

- Pretrained **ECAPA-TDNN** from [SpeechBrain](https://huggingface.co/speechbrain/spkrec-ecapa-voxceleb): `speechbrain/spkrec-ecapa-voxceleb`.
- Trained on VoxCeleb1+2; **EER 0.80%** on VoxCeleb1-test (cleaned).
- One model does:
  - **Embeddings:** `EncoderClassifier.from_hparams(...)` → `encode_batch(signal)` → 192‑dim vector.
  - **Verification:** `SpeakerRecognition.from_hparams(...)` → `verify_files(file1, file2)` → same/different speaker + score.

**Pros**

- **Fits our pipeline:** We already have diarization (OpenAI). We only need “audio → embedding.” SpeechBrain gives exactly that with a few lines.
- **No extra accounts:** Public Hugging Face model, no API token required for the model itself.
- **Simple API:** Load model once, `encode_batch(wav_tensor)` → return list of floats. Easy to wrap in a small HTTP service.
- **Verification built-in:** Optional “are these two clips the same speaker?” without touching our clustering.
- **Well documented:** [Speaker verification with ECAPA-TDNN](https://huggingface.co/speechbrain/spkrec-ecapa-voxceleb) and SpeechBrain tutorials.

**Cons**

- You run a **Python service** (e.g. FastAPI) that loads the model and exposes `/embed` (and optionally `/verify`). Not a single Node app.
- **Input:** 16 kHz mono. Our pipeline can send per-speaker slices; if your ingest is already 16 kHz mono (or you convert once), you’re good.

**Summary:** Best fit for “recognize different voices and group people by profile” when you already have diarization: add a small Python service that returns ECAPA-TDNN embeddings and optionally verification; keep the rest of the pipeline as-is.

---

## Option B: pyannote.audio

**What it is**

- [pyannote.audi
[truncated — 3361 more characters]
```

### package.json

```
{
  "name": "thirdparty",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3003",
    "build": "next build",
    "start": "next start -p 3003",
    "embedder": "cd services/speaker_embedder && uvicorn app:app --host 0.0.0.0 --port 5000",
    "diarizer": "cd services/pyannote && uvicorn app:app --host 0.0.0.0 --port 5010",
    "lint": "next lint",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "latest",
    "@google-cloud/speech": "^7.2.1",
    "@google/genai": "^1.41.0",
    "ffmpeg-static": "^5.2.0",
    "framer-motion": "^12.34.0",
    "lucide-react": "^0.564.0",
    "next": "^14.2.0",
    "openai": "^6.22.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "recharts": "^3.7.0",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@types/node": "^20.12.12",
    "@types/react": "^18.2.66",
    "@types/react-dom": "^18.2.22",
    "eslint": "^8.57.0",
    "eslint-config-next": "^14.2.0",
    "typescript": "^5.4.5"
  }
}

```

### services/pyannote/requirements.txt

```
fastapi
uvicorn
numpy
soundfile
torch
torchaudio
pyannote.audio

```

### services/speaker_embedder/requirements.txt

```
# SpeechBrain ECAPA-TDNN speaker embeddings + verification
# Python 3.8+
speechbrain>=1.0.0
torch>=2.0.0
torchaudio>=2.0.0
fastapi>=0.100.0
uvicorn[standard]>=0.22.0
python-multipart>=0.0.6
soundfile>=0.12.0
numpy>=1.24.0

```

### index.tsx

```typescript

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const rootElement = document.getElementById('root');
if (!rootElement) {
  throw new Error("Could not find root element to mount to");
}

const root = ReactDOM.createRoot(rootElement);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

```

### App.tsx

```typescript

import React, { useState } from 'react';
import { 
  Heart, 
  Wind, 
  Lock, 
  Menu, 
  X,
  Compass,
  Leaf,
  Navigation
} from 'lucide-react';
import Dashboard from './components/Dashboard';
import Mediator from './components/Mediator';
import Settings from './components/Settings';

enum Tab {
  Pulse = 'pulse',
  Bridge = 'bridge',
  Presence = 'presence',
  Privacy = 'privacy'
}

const App: React.FC = () => {
  const [activeTab, setActiveTab] = useState<Tab>(Tab.Pulse);
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

  const NavItem = ({ tab, icon: Icon, label }: { tab: Tab, icon: any, label: string }) => (
    <button
      onClick={() => {
        setActiveTab(tab);
        setIsMobileMenuOpen(false);
      }}
      className={`flex items-center gap-4 px-8 py-5 transition-all duration-700 w-full relative group ${
        activeTab === tab 
          ? 'text-[#2E2A25]' 
          : 'text-[#2E2A25]/20 hover:text-[#2E2A25]/50'
      }`}
    >
      <Icon className={`w-5 h-5 transition-all duration-700 ${activeTab === tab ? 'scale-110 text-[#E5989B]' : 'group-hover:scale-105'}`} />
      <span className="text-[10px] tracking-[0.4em] uppercase font-bold">{label}</span>
      {activeTab === tab && (
        <div className="absolute left-0 w-[2px] h-8 bg-[#E5989B] rounded-full shadow-[0_0_10px_rgba(229,152,155,0.2)]"></div>
      )}
    </button>
  );

  return (
    <div className="min-h-screen flex flex-col md:flex-row">
      {/* Mobile Navigation Bar */}
      <div className="md:hidden flex items-center justify-between p-8 bg-[#F5F1E8]/80 backdrop-blur-md sticky top-0 z-50 border-b border-[#2E2A25]/5">
        <div className="flex items-center gap-4">
          <div className="w-8 h-8 rounded-full border border-[#E5989B]/20 flex items-center justify-center serif italic font-bold text-[#E5989B]">3P</div>
          <span className="serif italic text-2xl text-[#2E2A25]/80">ThirdParty</span>
        </div>
        <button onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} className="p-2 text-[#2E2A25]/60">
          {isMobileMenuOpen ? <X /> : <Menu />}
        </button>
      </div>

      {/* Persistent Sidebar */}
      <aside className={`
        fixed inset-0 z-40 md:relative md:flex md:flex-col
        w-full md:w-80 lg:w-96 bg-[#F5F1E8] border-r border-[#2E2A25]/5
        transition-all duration-700 ease-in-out
        ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
      `}>
        <div className="hidden md:flex flex-col items-center py-20">
          <div className="w-16 h-16 rounded-full border border-[#E5989B]/20 flex items-center justify-center serif italic font-bold text-2xl mb-6 text-[#E5989B]/40 group hover:border-[#E5989B]/60 transition-all duration-1000">
             3P
          </div>
          <span className="text-3xl serif italic tracking-tight text-[#2E2A25]/80">ThirdParty</span>
          <span className="text-[10px] text-[#E5989B]/30 uppercase mt-3 tracking-[0.6em] font-bold">Sacred Space</span>
        </div>

        <nav className="flex-grow pt-8 space-y-2">
          <NavItem tab={Tab.Pulse} icon={Heart} label="The Pulse" />
          <NavItem tab={Tab.Bridge} icon={Compass} label="Bridge Builder" />
          <NavItem tab={Tab.Presence} icon={Wind} label="Presence" />
          <NavItem tab={Tab.Privacy} icon={Lock} label="Sanctuary" />
        </nav>

        <div className="p-10 space-y-10">
          <div className="p-8 bg-white/50 rounded-[2.5rem] border border-[#E5989B]/10 space-y-4">
            <div className="flex items-center gap-3">
              <Navigation className="w-3 h-3 text-[#E5989B]/60" />
              <span className="text-[9px] uppercase font-bold tracking-[0.4em] text-[#E5989B]/50">Current State</span>
            </div>
            <p className="text-sm text-[#2E2A25]/60 leading-relaxed italic">"A quiet, easeful resonance is settling between you."</p>
          </div>
          
          <div className="flex items-center gap-6 px-2 opacity-60 hover:opacity-100 transition-all cursor-pointer">
            <div className="w-12 h-12 rounded-full border border-[#E5989B]/10 overflow-hidden shadow-sm">
              <img src="https://picsum.photos/seed/sacred/150/150" alt="Shared Life" className="grayscale contrast-[0.9] group-hover:grayscale-0 transition-all" />
            </div>
            <div>
              <p className="text-sm font-bold serif italic text-[#2E2A25]/80">Alex & Jordan</p>
              <p className="text-[9px] text-[#E5989B]/40 uppercase tracking-[0.3em] font-black">United</p>
            </div>
          </div>
        </div>
      </aside>

      {/* Main Sanctuary Area */}
      <main className="flex-grow overflow-y-auto p-6 md:p-12 lg:p-24 relative">
        <div className="max-w-5xl mx-auto w-full h-full">
          {activeTab === Tab.Pulse && <Dashboard />}
          {activeTab === Tab.Bridge && <Mediator />}
          {activeTab === Tab.Privacy && <Settings />}
          {activeTab === Tab.Presence && (
            <div className="flex flex-col items-center justify-center h-[70vh] space-y-16 animate-in fade-in duration-1000">
              <div className="relative">
                <div className="w-72 h-72 border border-[#E5989B]/10 rounded-full flex items-center justify-center">
                   <div className="w-56 h-56 bg-[#E5989B]/5 rounded-full blur-[80px] animate-pulse"></div>
                   <Wind className="w-20 h-20 text-[#E5989B]/30 absolute" />
                </div>
              </div>
              <div className="text-center space-y-6">
                <h2 className="text-5xl serif italic font-light text-[#2E2A25]/80">Presence Flow</h2>
                <p className="text-[#2E2A25]/40 text-lg max-w-sm mx-auto leading-relaxed italic">
                  Take a breath. ThirdParty is holding space while you connect. Focus on the heartbeat of your relationship.
                </p>
              </div>
              <button className="px-14
[truncated — 372 more characters]
```

### app/page.tsx

```typescript
import { redirect } from "next/navigation";

export default function HomePage() {
  redirect("/timeline");
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";
import { BottomNav } from "@/components/BottomNav";
import { ConversationListener } from "@/components/ConversationListener";

export const metadata: Metadata = {
  title: "ThirdParty | A Space for Intimacy",
  description: "Relationship mirror and guided journaling coach"
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <script src="https://cdn.tailwindcss.com" async></script>
      </head>
      <body>
        <ConversationListener />
        <div className="min-h-screen pb-16">
          {children}
        </div>
        <BottomNav />
      </body>
    </html>
  );
}

```

### packages/shared/index.ts

```typescript
export * from "./types";

```

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