# Project export: Eloception

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: Eloception is an AI speaking trainer that gives near real-time feedback while you talk. It listens through your mic, transcribes your speech live, and suggests improvements to improve your accent.
- Devpost: https://devpost.com/software/speechcoach-vzynxl
- GitHub: https://github.com/AnnieWang314/treehacks-2026
- Team: 4 GitHub contributor(s) — Janet (21 commits), Cursor (6 commits), Annie Wang (4 commits), Richard Chen (2 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# Eloception

MVP web app for **near-real-time spoken feedback** on pronunciation and enunciation. Uses streaming ASR (Deepgram), AWS Polly TTS, and an LLM (OpenAI) to generate concise coaching tips.

## Repo structure (monorepo)

- **`/apps/web`** – Next.js (App Router) + TypeScript frontend
- **`/apps/server`** – Flask + Flask-SocketIO backend (Deepgram, OpenAI, AWS Polly)
- **`/packages/shared`** – Shared TypeScript types (optional for MVP)

## Prerequisites

- **Node.js** 18+ and npm (or pnpm)
- **Python** 3.11+
- **API keys**: Deepgram, OpenAI; **AWS credentials** for Polly TTS

## Setup

### 1. Backend (Flask server)

```bash
cd apps/server
python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env and set:
#   DEEPGRAM_API_KEY=...
#   OPENAI_API_KEY=...
#   AWS_ACCESS_KEY_ID=... / AWS_SECRET_ACCESS_KEY=... / AWS_REGION=...
#   AWS_POLLY_VOICE_ID=...   # optional; default is Joanna
```

**Important for Flask-SocketIO:** Run the server with **eventlet** so WebSockets work:

```bash
# From apps/server with venv activated:
python app.py
# Server listens on port 5001 by default (set PORT=5002 etc. if needed). On macOS, 5000 is often used by AirPlay.
```

The app is written for `async_mode="eventlet"`, so ensure `eventlet` is installed (`pip install eventlet` is in `requirements.txt`). If you use `flask run`, it may use the development server; for production or reliable WebSocket handling, run with the `socketio.run(app, ...)` pattern above.

### 2. Frontend (Next.js)

```bash
# From repo root
npm install
cp apps/web/.env.local.example apps/web/.env.local
# Edit apps/web/.env.local and set:
#   NEXT_PUBLIC_WS_URL=http://localhost:5001
npm run dev:web
```

### 3. Run both (from repo root)

```bash
npm install
npm run dev
```

This runs the backend (Flask on port 5001) and the web app (Next.js on port 3000) concurrently. Ensure `apps/server/.env` is set as above.

## Env vars

| Var | Where | Description |
|-----|--------|--------------|
| `DEEPGRAM_API_KEY` | server | Deepgram API key for live transcription |
| `OPENAI_API_KEY` | server | OpenAI API key for coaching tips |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` | server | AWS credentials for Polly TTS |
| `AWS_POLLY_VOICE_ID` | server | Polly voice (e.g. Joanna, Matthew; default in `.env.example`) |
| `NEXT_PUBLIC_WS_URL` | web | Backend URL for Socket.IO (e.g. `http://localhost:5001`) |

## Usage

1. Open the web app (e.g. http://localhost:3000).
2. Click **Start** and allow microphone access.
3. Speak; live captions appear. After each phrase (silence ≥800ms or Deepgram final), you get:
   - Metrics: WPM, filler count, low-confidence words
   - A short coaching tip + drill phrase
   - Spoken feedback via AWS Polly TTS (barge-in: if you speak again while TTS is playing, it stops).

## Troubleshooting

- **CORS:** Backend allows `http://localhost:3000` and `http://127.0.0.1:3000`. If you use another origin, add it in `app.py` (`CORS(app, origins=[...])`).
- **Mic permissions:** Ensure the browser has microphone access and that you’re on HTTPS or localhost.
- **Port 5000 in use (macOS):** On macOS Monterey and later, port 5000 is often used by AirPlay Receiver. The app defaults to **port 5001**; set `NEXT_PUBLIC_WS_URL=http://localhost:5001` in the web app and run the server with `python app.py` (or `PORT=5002 python app.py` to use another port).
- **WebSocket / transport:** Socket.IO will try WebSocket then polling. If the backend is not the same host as the frontend, set `NEXT_PUBLIC_WS_URL` to the full backend URL (no trailing slash). For Flask-SocketIO, use **eventlet** (or gevent) in dev so WebSockets work.
- **No transcription:** Check `DEEPGRAM_API_KEY` and that the server is receiving binary `audio_chunk` events. Server expects 16 kHz mono 16-bit PCM.
- **No tips / TTS:** Check `OPENAI_API_KEY` and AWS credentials for Polly. Tips are rate-limited (about once per 2 seconds per phrase).

## Tech summary

- **Frontend:** Next.js 14 (App Router), TypeScript, Socket.IO client, Web Audio API (mic → 16 kHz mono PCM), minimal UI with live transcript, last tip, and metrics.
- **Backend:** Flask, Flask-SocketIO (eventlet), Deepgram live streaming, phrase-boundary logic (final + 800ms silence, debounced), OpenAI for tip + drill, AWS Polly TTS, barge-in (tts_stop on new audio).

No database; MVP scope only.


## Detected evidence (automated analysis)

Indexed codebase: 18 recognized source files, 118 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Flask (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
- AI coding agent: Cursor — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (29 of 29)

```
.DS_Store
.gitignore
apps/.cursor/debug.log
apps/server/.env.example
apps/server/accent_info.json
apps/server/app.py
apps/server/db.py
apps/server/requirements.txt
apps/server/run.sh
apps/server/utils.py
apps/web/.env
apps/web/app/globals.css
apps/web/app/layout.tsx
apps/web/app/page.tsx
apps/web/next-env.d.ts
apps/web/next.config.js
apps/web/package.json
apps/web/public/audio-worklet-processor.js
apps/web/tsconfig.json
model/__init__.py
model/classifier.py
model/config.py
model/README.md
model/test_inference.py
package.json
packages/shared/index.ts
packages/shared/package.json
README.md
SETUP.md
```

### Dependencies

- apps/server/requirements.txt: anthropic@>=0.39.0, boto3@>=1.34.0, deepgram-sdk@>=3.0.0, elevenlabs@>=1.0.0, eventlet@>=0.35.0, flask@>=3.0.0, flask-cors@>=4.0.0, flask-socketio@>=5.3.0, numpy@>=1.24.0, openai@>=1.0.0, python-dotenv@>=1.0.0, python-engineio@>=4.8.0, python-socketio@>=5.10.0, speechbrain@>=1.0.0, torch@>=2.0.0, torchaudio@>=2.0.0, transformers@>=4.30.0
- apps/web/package.json: @types/node@^20.0.0, @types/react@^18.2.0, @types/react-dom@^18.2.0, next@14.2.0, react@^18.2.0, react-dom@^18.2.0, socket.io-client@^4.7.0, typescript@^5.0.0
- package.json: concurrently@^8.2.0, openai@^6.22.0

### Recent commits (newest first)

- change name
- Merge pull request #4 from AnnieWang314/janet
- clean infobar
- Merge branch 'janet' of github.com:AnnieWang314/treehacks-2026 into janet
- Refactor TTS model management and accent handling
- - Updated .env.example to reflect ElevenLabs as the default TTS provider and added optional accent voice IDs.
- Merge remote-tracking branch 'origin/janet'
- conversation in practice
- :wq#
- feat(accent): enhance accent detection with improved classifier integration
- feat(accent): integrate CommonAccent classifier for accent detection
- Merge pull request #3 from AnnieWang314/janet
- centralising utils, fixing asr transcription options
- feat(asr): refactor ASR and TTS integration with utils, update environment configuration
- Merge pull request #2 from AnnieWang314/janet
- feat(asr): optional custom accent classifier
- feat(asr): Deepgram diarization and utterances
- Merge pull request #1 from AnnieWang314/janet
- feat(converse): real-time voice conversation with agent
- feat(converse): implement queue handling for Converse mode and adjust tip generation logic

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

### SETUP.md

```markdown
# SpeechCoach – Quick setup

## 1. Backend env keys

In **`apps/server/`**:

```bash
cd apps/server
cp .env.example .env
```

Edit **`apps/server/.env`** and set real values:

| Variable | Where to get it |
|----------|-----------------|
| `DEEPGRAM_API_KEY` | [Deepgram](https://console.deepgram.com/) → API Keys |
| `OPENAI_API_KEY` | [OpenAI](https://platform.openai.com/api-keys) |
| `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` | [AWS IAM](https://console.aws.amazon.com/iam/) → Users → Security credentials (or use `~/.aws/credentials`) |
| `AWS_REGION` | e.g. `us-east-1` |
| `AWS_POLLY_VOICE_ID` | Optional. Default `Joanna`. See [Polly voices](https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). |

Leave `AWS_POLLY_VOICE_ID` as-is if you don’t care which voice is used.

## 2. Install and run backend

From **`apps/server/`** (with your venv activated):

```bash
pip install -r requirements.txt
python app.py
```

You should see the Flask app listening on **http://0.0.0.0:5001**.

## 3. Frontend

Your **`apps/web/.env.local`** already has:

```env
NEXT_PUBLIC_WS_URL=http://localhost:5001
```

From the **repo root**:

```bash
npm install
npm run dev:web
```

Or run both backend + frontend:

```bash
npm run dev
```

## 4. Use the app

1. Open **http://localhost:3000** in the browser.
2. Click **Start** and allow microphone access.
3. Speak; you should see live captions, then tips and TTS after each phrase.

---

**Summary:** Fill in Deepgram, OpenAI, and AWS credentials (and optionally Polly voice ID) in `apps/server/.env`, then run the server and the web app. No database or extra config needed.

```

### package.json

```
{
  "name": "speechcoach",
  "private": true,
  "scripts": {
    "dev": "concurrently \"npm run dev:server\" \"npm run dev:web\"",
    "dev:web": "npm run dev -w apps/web",
    "dev:server": "cd apps/server && python app.py",
    "install:all": "npm install && cd apps/server && pip install -r requirements.txt"
  },
  "workspaces": [
    "apps/web",
    "packages/shared"
  ],
  "devDependencies": {
    "concurrently": "^8.2.0"
  },
  "dependencies": {
    "openai": "^6.22.0"
  }
}

```

### packages/shared/package.json

```
{
  "name": "@speechcoach/shared",
  "version": "0.0.1",
  "private": true,
  "main": "index.ts",
  "types": "index.ts"
}

```

### apps/server/requirements.txt

```
flask>=3.0.0
flask-socketio>=5.3.0
flask-cors>=4.0.0
python-socketio>=5.10.0
python-engineio>=4.8.0
eventlet>=0.35.0
deepgram-sdk>=3.0.0
openai>=1.0.0
anthropic>=0.39.0
elevenlabs>=1.0.0
boto3>=1.34.0
numpy>=1.24.0
python-dotenv>=1.0.0

# Accent model (CommonAccent / model package). Install PyTorch first if needed (e.g. --index-url https://download.pytorch.org/whl/cu121 or /cpu).
torch>=2.0.0
torchaudio>=2.0.0
speechbrain>=1.0.0
transformers>=4.30.0

```

### apps/web/package.json

```
{
  "name": "speechcoach-web",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "14.2.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "socket.io-client": "^4.7.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "typescript": "^5.0.0"
  }
}

```

### packages/shared/index.ts

```typescript
/**
 * Shared types for SpeechCoach (mirror backend events)
 */

export interface TranscriptPartial {
  text: string;
}

export interface WordInfo {
  word: string;
  start: number;
  end: number;
  confidence?: number;
}

export interface TranscriptFinal {
  text: string;
  words: WordInfo[];
}

export interface Metrics {
  wpm: number;
  filler_count: number;
  low_conf_words: string[];
}

export interface Tip {
  tip: string;
  drill: string;
}

export interface TtsAudio {
  mime: string;
  data_base64: string;
}

```

### apps/web/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { DM_Sans } from "next/font/google";
import "./globals.css";

const dmSans = DM_Sans({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "SpeechCoach",
  description: "Near-real-time spoken feedback on pronunciation and enunciation",
};

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

```

### apps/server/app.py

```python
"""
SpeechCoach backend: Flask-SocketIO server with Deepgram live transcription,
coaching tips and TTS via utils (env-driven: OpenAI/Claude, ElevenLabs/Polly).
Phrase-boundary detection and barge-in.
"""
import os
import time
import base64
import threading
import queue
from collections import deque

from dotenv import load_dotenv
load_dotenv()

from flask import Flask, request
from flask_cors import CORS
from flask_socketio import SocketIO, emit

from utils import (
    generate_tts,
    get_coaching_tip,
    get_chat_reply,
    classify_accent,
    preload_accent_model,
    get_openai_voice_display_info,
    resample_audio_48k_to_16k,
    get_asr_client,
    get_asr_connect_options,
)
from db import (
    init_db,
    create_conversation,
    get_recent_turn_context,
    save_conversation_turn,
)

# --- Config ---
DEBUG = os.environ.get("DEBUG", "").lower() in ("1", "true", "yes")
DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY", "")
ENABLE_ACCENT_CLASSIFIER = os.environ.get("ENABLE_ACCENT_CLASSIFIER", "").lower() in ("1", "true", "yes")
ACCENT_MAX_BUFFER_SEC = 60

FILLERS = ["um", "uh", "like", "you know", "sort of", "kind of"]
LOW_CONF_THRESHOLD = 0.80
PHRASE_SILENCE_MS = 800
PHRASE_DEBOUNCE_SEC = 2.0
MIN_PHRASE_WORDS = 1

app = Flask(__name__)
CORS(app, origins=["http://localhost:3000", "http://127.0.0.1:3000"])
socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")
init_db()

# Per-connection state (keyed by session id from socket)
sessions = {}
sessions_lock = threading.Lock()

# Queue for transcript events: Deepgram thread puts (sid, event_name, payload) here;
# drainer greenlet in eventlet hub emits to client so Socket.IO actually delivers.
transcript_out_queue = queue.Queue()
transcript_drainer_started = False

# Queue for Converse mode: (sid, user_text) when a phrase is final in converse mode.
converse_pending_queue = queue.Queue()
converse_worker_started = False

# Queue for accent classifier: (sid, phrase_start_sec, phrase_end_sec) when phrase is final and accent enabled.
accent_pending_queue = queue.Queue()
accent_worker_started = False

# Load accent model at startup when enabled so it's ready before first speech.
if ENABLE_ACCENT_CLASSIFIER and os.environ.get("ACCENT_PROVIDER", "").lower() == "commonaccent":
    preload_accent_model()


def get_session(sid):
    with sessions_lock:
        return sessions.get(sid)


def set_session(sid, data):
    with sessions_lock:
        sessions[sid] = data


def clear_session(sid):
    with sessions_lock:
        sessions.pop(sid, None)


def ensure_session(sid):
    s = get_session(sid)
    if s is None:
        s = {
            "dg_socket": None,
            "dg_ready": False,
            "dg_stop": False,
            "phrase_buffer": [],
            "current_partial": "",
            "last_audio_time": 0,
            "last_phrase_time": 0,
            "tts_playing": False,
            "last_tip_time": 0,
            "deepgram_started": False,
            "conversation_id": None,
            "conversation_mode": None,
            "conversation_exercise": None,
            "tip_history": [],
        }
        set_session(sid, s)
    return s


# --- Deepgram live (SDK v5: listen.v1.connect context manager + recv loop in thread) ---
def _transcript_drainer():
    """Run in eventlet hub: drain transcript_out_queue and emit to clients."""
    import eventlet
    while True:
        try:
            sid, event_name, payload = transcript_out_queue.get_nowait()
            socketio.emit(event_name, payload, room=sid)
            if DEBUG or event_name != "transcript_partial":
                print(f"[SpeechCoach] drainer emitted {event_name} to sid={sid}")
        except queue.Empty:
            pass
        except Exception as e:
            print(f"[SpeechCoach] drainer error: {e}")
        eventlet.sleep(0.02)


def _handle_dg_result(sid, result):
    """Process a ListenV1ResultsEvent; queue emit for drainer (thread-safe)."""
    channel = getattr(result, "channel", None)
    if not channel or not getattr(channel, "alternatives", None):
        return
    alt = channel.alternatives[0]
    transcript = (getattr(alt, "transcript", None) or "").strip()
    if not transcript:
        return
    is_final = getattr(result, "is_final", False) or getattr(result, "speech_final", False)
    words = []
    if getattr(alt, "words", None):
        for w in alt.words:
            word_obj = {
                "word": getattr(w, "word", str(w)),
                "start": getattr(w, "start", 0) or 0,
                "end": getattr(w, "end", 0) or 0,
                "confidence": getattr(w, "confidence", None),
            }
            if getattr(w, "speaker", None) is not None:
                word_obj["speaker"] = getattr(w, "speaker", None)
            words.append(word_obj)
    confidence = getattr(alt, "confidence", None)
    if is_final:
        transcript_out_queue.put((
            sid,
            "transcript_final",
            {
                "text": transcript,
                "words": words if words else [{"word": transcript, "start": 0, "end": 0, "confidence": confidence}],
            },
        ))
        session = get_session(sid)
        if session:
            if session.get("mode") == "converse":
                converse_pending_queue.put((sid, transcript))
            else:
                session["phrase_buffer"].append({"text": transcript, "words": words, "confidence": confidence})
            if ENABLE_ACCENT_CLASSIFIER and words:
                try:
                    phrase_start = min((w.get("start", 0) or 0) for w in words)
                    phrase_end = max((w.get("end", 0) or 0) for w in words)
                    if phrase_end > phrase_start:
                        accent_pending_queue.put((sid, phrase_start, phrase_end))
                except Exception:
                    pass
            session["last_phrase_time"] = time.time()
            session["current_partial"] = ""
            set_
[truncated — 20691 more characters]
```

### apps/web/app/page.tsx

```typescript
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";

const WS_URL = process.env.NEXT_PUBLIC_WS_URL || "http://localhost:5001";

type TabId = "practice" | "converse";

const EXERCISE_CATEGORIES: Record<string, string[]> = {
  Enunciation: ["Vowel sounds", "Common consonant sounds", "Tongue twisters"],
  Clarity: ["Pacing", "Articulation", "Tongue twisters"],
  "Accent training": ["US", "UK", "AU", "IN", "SG", "NZ"],
};

const ACCENT_LABELS: Record<string, string> = {
  US: "American (US)",
  UK: "British (UK)",
  AU: "Australian (AU)",
  IN: "Indian (IN)",
  SG: "Singaporean (SG)",
  NZ: "New Zealand (NZ)",
};

const DEFAULT_PROMPT = "Say something in your own words.";

function PlayIcon() {
  return (
    <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
      <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" />
    </svg>
  );
}

function getExamplePrompt(category: string | null, subtask: string | null): string {
  if (!category || !subtask) return DEFAULT_PROMPT;
  const key = `${category}|${subtask}`;
  const map: Record<string, string> = {
    "Accent training|US": "The weather in the United States varies a lot from coast to coast.",
    "Accent training|UK": "The weather in London is quite pleasant today.",
    "Accent training|AU": "It is a beautiful day to go to the beach.",
    "Accent training|IN": "I will call you back after the meeting.",
    "Accent training|SG": "Please send the report by the end of the day.",
    "Accent training|NZ": "We are going to the park this afternoon.",
    "Enunciation|Vowel sounds": "She sells seashells by the seashore.",
    "Enunciation|Common consonant sounds": "Peter Piper picked a peck of pickled peppers.",
    "Enunciation|Tongue twisters": "How much wood would a woodchuck chuck?",
    "Clarity|Pacing": "Take your time and speak clearly.",
    "Clarity|Articulation": "Repeat each word distinctly and at a steady pace.",
    "Clarity|Tongue twisters": "Unique New York, New York unique.",
  };
  return map[key] ?? DEFAULT_PROMPT;
}

export default function Home() {
  const [tab, setTab] = useState<TabId>("practice");
  const [connected, setConnected] = useState(false);
  const [active, setActive] = useState(false);
  const [practiceCategory, setPracticeCategory] = useState<string | null>(null);
  const [practiceSubtask, setPracticeSubtask] = useState<string | null>(null);
  const [converseAccent, setConverseAccent] = useState<string | null>(null);
  const [conversationLog, setConversationLog] = useState<{ role: "user" | "assistant"; text: string }[]>([]);
  const [practiceChatLog, setPracticeChatLog] = useState<
    { role: "user" | "assistant"; text: string; drill?: string }[]
  >([]);
  const [isGeneratingTip, setIsGeneratingTip] = useState(false);
  const [accentResult, setAccentResult] = useState<{
    accent: string;
    confidence?: number;
    top_3?: { accent: string; confidence: number }[];
  } | null>(null);
  const [transcriptFinal, setTranscriptFinal] = useState("");
  const [transcriptPartial, setTranscriptPartial] = useState("");
  const [metrics, setMetrics] = useState<{
    wpm: number;
    filler_count: number;
    low_conf_words: string[];
  } | null>(null);
  const [ttsInfo, setTtsInfo] = useState<{ model: string; voice: string; instructions: string | null } | null>(null);
  const [currentPrompt, setCurrentPrompt] = useState<string>(DEFAULT_PROMPT);

  const socketRef = useRef<Socket | null>(null);
  const streamRef = useRef<MediaStream | null>(null);
  const audioContextRef = useRef<AudioContext | null>(null);
  const workletNodeRef = useRef<AudioWorkletNode | null>(null);
  const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
  const audioElRef = useRef<HTMLAudioElement | null>(null);
  const startedConverseRef = useRef(false);
  const tabRef = useRef<TabId>("practice");
  const tipTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  function arrayBufferToBase64(buffer: ArrayBuffer): string {
    const bytes = new Uint8Array(buffer);
    let binary = "";
    for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
    return typeof btoa !== "undefined" ? btoa(binary) : "";
  }

  const connect = useCallback(() => {
    if (socketRef.current) return;
    const socket = io(WS_URL, {
      transports: ["websocket", "polling"],
      reconnection: true,
    });
    socket.on("connect", () => setConnected(true));
    socket.on("disconnect", () => {
      setConnected(false);
      setIsGeneratingTip(false);
      if (tipTimeoutRef.current) {
        clearTimeout(tipTimeoutRef.current);
        tipTimeoutRef.current = null;
      }
    });
    socket.on("transcript_partial", (data: { text: string }) => {
      setTranscriptPartial(data.text || "");
      console.log("[SpeechCoach] transcript_partial:", data.text || "");
    });
    socket.on("transcript_final", (data: { text: string }) => {
      const text = (data.text || "").trim();
      if (startedConverseRef.current && text) {
        setConversationLog((prev) => [...prev, { role: "user", text }]);
      }
      setTranscriptFinal((prev) => {
        const newTranscript = (prev ? `${prev} ${text}` : text).trim();
        console.log("[SpeechCoach] transcript_final:", newTranscript);
        return newTranscript;
      });
      setTranscriptPartial("");
    });
    socket.on("metrics", (data: { wpm: number; filler_count: number; low_conf_words: string[] }) =>
      setMetrics(data)
    );
    socket.on("tip", (data: { tip: string; drill: string }) => {
      if (tipTimeoutRef.current) {
        clearTimeout(tipTimeoutRef.current);
        tipTimeoutRef.current = null;
      }
      setIsGeneratingTip(false);
      if (tabRef.current === "practice" && (data.tip || dat
[truncated — 18328 more characters]
```

### model/__init__.py

```python
"""
model/ — Accent classification module.

Usage:
    from model import AccentClassifier, AccentResult

    clf = AccentClassifier()
    result = clf.classify("audio.wav")
"""

from .classifier import AccentClassifier, AccentResult
from .config import TARGET_ACCENTS, DISPLAY_TO_MODEL, NUM_TARGET_ACCENTS

__all__ = [
    "AccentClassifier",
    "AccentResult",
    "TARGET_ACCENTS",
    "DISPLAY_TO_MODEL",
    "NUM_TARGET_ACCENTS",
]

```

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