# Project export: Clinical Interpretation

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: UC Berkeley AI Hackathon 2026
- Tagline: An accurate, cost-effective tool to bridge the language gap in medical contexts
- Devpost: https://devpost.com/software/clinical-interpretation
- GitHub: https://github.com/dean8bel/clinical-interpreter
- Video: https://www.youtube.com/embed/f2zhc2dGZCc?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — dean8bel (1 commits)

## Devpost submission (written by the team)

### Inspiration

Language barriers are one of the most persistent and high-impact challenges in clinical care, especially in underserved and volunteer-based healthcare settings. As a medical assistant at a clinic, I frequently work with patients who are not fluent in English. In many cases, the current solution is to use human interpreters over the phone or video call to translate during patient visits. While effective, this approach introduces significant cost and logistical overhead, particularly in volunteer or low-resource environments where interpreter services must be scheduled and paid per session. Over time, these costs accumulate and can become difficult to sustain, limiting access to consistent interpretation services when they are needed most. This project aims to reduce that burden by providing a real-time, low-cost alternative for medical interpretation, enabling clinicians and patients to communicate directly without sacrificing clarity or safety.

### What it does

Clinical Interpretation is a real-time bilingual voice system that enables seamless communication between English- and Spanish-speaking patients and providers. Captures live speech from either party Transcribes audio using Deepgram streaming speech-to-text Detects language and translates speech using an LLM Converts translated text into natural speech using Deepgram TTS Delivers real-time bidirectional voice translation during the conversation The result is a fluid, interpreter-like experience that supports natural clinical dialogue without a human intermediary.

### How we built it

We built a streaming voice pipeline using: Deepgram Nova / Flux (STT) for real-time multilingual transcription Claude / GPT-4o-mini for strict, deterministic medical translation Deepgram Aura TTS for natural speech output in English and Spanish Node.js for orchestrating real-time audio streams Speaker + microphone streaming libraries to handle live audio input/output

### Challenges we ran into

Handling real-time audio streaming without buffer underruns or audio cutouts Managing backpressure between streaming STT and speaker output Ensuring the LLM behaves strictly as a translator (not a conversational agent) Designing reliable bidirectional switching between speaker roles Preventing latency and audio overlap in a continuous conversation loop

### Accomplishments we're proud of

Built a fully functional real-time medical voice interpreter Achieved stable bidirectional English ↔ Spanish communication Successfully integrated STT, LLM translation, and TTS into a single low-latency system Implemented buffering logic to eliminate audio cutoffs and improve stream reliability Created a system that closely mirrors real-world clinical interpreter workflows

### What we learned

Real-time voice systems fail more often due to streaming and buffering issues than model quality In clinical communication, clarity and reliability matter more than conversational intelligence Role separation (doctor vs patient) is more important than language detection alone LLMs must be tightly constrained to behave as deterministic translators in healthcare settings Building voice AI requires thinking in terms of event-driven audio pipelines, not request-response APIs

### What's next

Add structured clinical documentation generation (SOAP notes and visit summaries) Expand language support beyond English and Spanish Improve role detection (automatic doctor/patient inference) Add support for interruption handling (“barge-in” during speech) Integrate with electronic health record (EHR) systems for clinical documentation Optimize latency for near-instant conversational response in real clinical environments

## README (from the GitHub repository)

# Inspiration

Language barriers are one of the most persistent and high-impact challenges in clinical care, especially in underserved and volunteer-based healthcare settings.

As a medical assistant at a clinic, I frequently work with patients who are not fluent in English. In many cases, the current solution is to use human interpreters over the phone or video call to translate during patient visits. While effective, this approach introduces significant cost and logistical overhead, particularly in volunteer or low-resource environments where interpreter services must be scheduled and paid per session.

Over time, these costs accumulate and can become difficult to sustain, limiting access to consistent interpretation services when they are needed most.

This project aims to reduce that burden by providing a real-time, low-cost alternative for medical interpretation, enabling clinicians and patients to communicate directly without sacrificing clarity or safety.

# What it Does

Clinical Interpretation is a real-time bilingual voice system that enables seamless communication between English- and Spanish-speaking patients and providers.

The system:

* Captures live speech from either party
* Transcribes audio using Deepgram streaming speech-to-text
* Detects language and translates speech using an LLM
* Converts translated text into natural speech using Deepgram TTS
* Delivers real-time bidirectional voice translation during the conversation

The result is a fluid, interpreter-like experience that supports natural clinical dialogue without a human intermediary.

# How We Built It

We built a streaming voice pipeline using:

* Deepgram Nova / Flux (STT) for real-time multilingual transcription
* GPT-4o-mini for deterministic medical translation
* Deepgram Aura TTS for natural speech output in English and Spanish
* Node.js for orchestrating real-time audio streams
* Speaker and microphone streaming libraries to handle live audio input/output

The system operates as a continuous pipeline:

**Speech → Speech-to-Text → Translation → Text-to-Speech → Playback**

# Challenges We Ran Into

* Handling real-time audio streaming without buffer underruns or audio cutouts
* Managing backpressure between streaming STT and speaker output
* Ensuring the LLM behaves strictly as a translator rather than a conversational agent
* Designing reliable bidirectional switching between speaker roles
* Preventing latency and audio overlap in a continuous conversation loop

# Accomplishments That We're Proud Of

* Built a fully functional real-time medical voice interpreter
* Achieved stable bidirectional English ↔ Spanish communication
* Successfully integrated STT, LLM translation, and TTS into a single low-latency system
* Implemented buffering logic to improve stream reliability and reduce audio cutoffs
* Created a system that closely mirrors real-world clinical interpreter workflows

# What We Learned

* Real-time voice systems fail more often due to streaming and buffering issues than model quality
* In clinical communication, clarity and reliability matter more than conversational intelligence
* Role separation (doctor vs. patient) is more important than language detection alone
* LLMs must be tightly constrained to behave as deterministic translators in healthcare settings
* Building voice AI requires thinking in terms of event-driven audio pipelines rather than traditional request-response APIs

# What's Next for Clinical Interpretation

* Generate structured clinical documentation such as SOAP notes, visit summaries, and translated after-visit instructions
* Expand language support beyond English and Spanish
* Improve role detection and speaker attribution during conversations
* Add interruption handling ("barge-in") for more natural dialogue
* Integrate directly with EHR systems to fit within existing clinical workflows
* Use patient context from the EHR (medications, diagnoses, allergies, labs) to improve translation accuracy
* Support multimodal inputs including PDFs, medical records, discharge paperwork, lab reports, imaging reports, and photographs
* Allow patients to upload test results and ask questions in their preferred language
* Provide translated explanations of medical documents to improve patient understanding and health literacy
* Optimize latency for near-instant conversational response in real clinical environments


## Detected evidence (automated analysis)

Indexed codebase: 14 recognized source files, 52 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (17 of 17)

```
.env.example
.gitignore
.nvmrc
lib/config.js
lib/createInterpreterSession.js
lib/deepgramClient.js
lib/network.js
LICENSE.md
main.js
package.json
pnpm-workspace.yaml
public/app.js
public/index.html
public/styles.css
README.md
scripts/doctor.js
server.js
```

### Dependencies

- package.json: @deepgram/sdk@5.4.0, dotenv@17.4.2, express@5.1.0, mic@2.1.2, speaker@0.5.5, ws@8.18.2

### Recent commits (newest first)

- Update README.md
- Update README.md
- Add bilingual medical interpreter demo with web UI and network fixes.
- initial
- feat: workshop scaffolding

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

### LICENSE.md

```markdown
MIT License

Copyright (c) 2023 Deepgram

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

### package.json

```
{
  "name": "uc-berkeley-ai-workshop-2026",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "main": "main.js",
  "engines": {
    "node": ">=22"
  },
  "scripts": {
    "start": "node main.js",
    "demo": "node server.js",
    "doctor": "node scripts/doctor.js"
  },
  "dependencies": {
    "@deepgram/sdk": "5.4.0",
    "dotenv": "17.4.2",
    "express": "5.1.0",
    "mic": "2.1.2",
    "speaker": "0.5.5",
    "ws": "8.18.2"
  }
}

```

### main.js

```javascript
/**
 * CLI medical interpreter — mic + speaker via Node.
 * For the browser demo, run: pnpm demo
 */

import "dotenv/config";
import "./lib/network.js";
import micFactory from "mic";
import Speaker from "speaker";
import { CHANNELS, BIT_DEPTH, SAMPLE_RATE } from "./lib/config.js";
import { createInterpreterSession } from "./lib/createInterpreterSession.js";

const main = async () => {
  const speaker = new Speaker({
    channels: CHANNELS,
    bitDepth: BIT_DEPTH,
    sampleRate: SAMPLE_RATE,
    signed: true,
  });

  const queue = [];
  let playing = false;

  function playNext() {
    if (playing || queue.length === 0) return;

    playing = true;
    const chunk = queue.shift();
    const ok = speaker.write(chunk);

    if (!ok) {
      speaker.once("drain", () => {
        playing = false;
        playNext();
      });
    } else {
      playing = false;
      setImmediate(playNext);
    }
  }

  const log = (event) => {
    switch (event.type) {
      case "status":
        console.log(`>> ${event.message}`);
        break;
      case "transcript":
        console.log(`[${event.role}] ${event.content}`);
        break;
      case "tts":
        console.log(`>> TTS model → ${event.model}`);
        break;
      case "userLanguage":
        console.log(
          `>> User language: ${event.lang} (transcript: "${event.transcript}")`,
        );
        break;
      case "languages":
        console.log(`>> Detected languages: ${event.languages.join(", ")}`);
        break;
      case "nextSpeaker":
        console.log(
          `>> Next speaker expected: ${event.speaker === "patient" ? "patient (Spanish)" : "doctor (English)"}`,
        );
        break;
      case "error":
        console.log(`>> Agent error: ${event.code ?? "unknown"} - ${event.message}`);
        break;
      default:
        break;
    }
  };

  console.log("Connecting to Deepgram agent...");
  const session = await createInterpreterSession({
    apiKey: process.env.DEEPGRAM_API_KEY,
    onEvent: log,
    onAudio: (chunk) => {
      queue.push(chunk);
      playNext();
    },
  });

  await session.start();

  const microphone = micFactory({
    rate: String(SAMPLE_RATE),
    channels: String(CHANNELS),
    bitwidth: String(BIT_DEPTH),
    encoding: "signed-integer",
    endian: "little",
    device: "default",
  });

  const micStream = microphone.getAudioStream();
  micStream.on("data", (chunk) => session.sendAudio(chunk));
  micStream.on("error", (error) => console.log(`>> Microphone error: ${error}`));

  microphone.start();
  console.log("\nListening... press Ctrl+C to exit.\n");

  const shutdown = () => {
    console.log("\nShutting down...");
    session.stop();
    microphone.stop();
    speaker.end();
    process.exit(0);
  };

  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);
};

main().catch((error) => {
  console.error(error);
  process.exit(1);
});

```

### server.js

```javascript
import "dotenv/config";
import "./lib/network.js";
import { DEEPGRAM_AGENT_HOST, verifyDeepgramDns } from "./lib/network.js";
import { createServer } from "node:http";
import express from "express";
import { WebSocketServer } from "ws";
import { createInterpreterSession } from "./lib/createInterpreterSession.js";

const PORT = Number(process.env.PORT) || 3000;

const networkErrorMessage = (error) => {
  if (error?.code === "ENOTFOUND" || error?.cause?.code === "ENOTFOUND") {
    return (
      `Cannot reach ${DEEPGRAM_AGENT_HOST}. Try: pnpm doctor\n` +
      `Quick fix: switch to a phone hotspot, or add to /etc/hosts after running nslookup ${DEEPGRAM_AGENT_HOST}`
    );
  }
  return error?.message ?? String(error);
};

const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });

app.use(express.static("public"));

wss.on("connection", (ws) => {
  let session = null;
  let starting = false;

  const send = (payload) => {
    if (ws.readyState === ws.OPEN) {
      ws.send(JSON.stringify(payload));
    }
  };

  ws.on("message", async (data, isBinary) => {
    if (isBinary) {
      session?.sendAudio(data);
      return;
    }

    let msg;
    try {
      msg = JSON.parse(data.toString());
    } catch {
      send({ type: "error", message: "Invalid message" });
      return;
    }

    if (msg.type === "start") {
      if (starting || session) return;
      starting = true;

      if (!process.env.DEEPGRAM_API_KEY) {
        send({ type: "error", message: "DEEPGRAM_API_KEY is not set in .env" });
        starting = false;
        return;
      }

      try {
        session = await createInterpreterSession({
          apiKey: process.env.DEEPGRAM_API_KEY,
          onEvent: send,
          onAudio: (chunk) => {
            if (ws.readyState === ws.OPEN) ws.send(chunk);
          },
        });
        await session.start();
      } catch (error) {
        send({ type: "error", message: networkErrorMessage(error) });
        session?.stop();
        session = null;
      } finally {
        starting = false;
      }
      return;
    }

    if (msg.type === "stop") {
      session?.stop();
      session = null;
      send({ type: "status", message: "Session ended" });
    }
  });

  ws.on("close", () => {
    session?.stop();
    session = null;
  });
});

server.on("error", (error) => {
  if (error.code === "EADDRINUSE") {
    console.error(
      `\nPort ${PORT} is already in use. Either:\n` +
        `  kill the old server:  lsof -ti :${PORT} | xargs kill -9\n` +
        `  or use another port:  PORT=3001 pnpm demo\n`,
    );
    process.exit(1);
  }
  throw error;
});

server.listen(PORT, async () => {
  console.log(`Demo running at http://localhost:${PORT}`);
  try {
    const { address } = await verifyDeepgramDns();
    console.log(`Reachable: ${DEEPGRAM_AGENT_HOST} → ${address}`);
  } catch {
    console.warn(
      `\nWarning: Cannot resolve ${DEEPGRAM_AGENT_HOST}. Run: pnpm doctor\n`,
    );
  }
});

```

### public/app.js

```javascript
const SAMPLE_RATE = 16000;

const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
const statusDot = document.getElementById("statusDot");
const statusText = document.getElementById("statusText");
const nextSpeakerEl = document.getElementById("nextSpeaker");
const patientLog = document.getElementById("patientLog");
const doctorLog = document.getElementById("doctorLog");
const interpreterLog = document.getElementById("interpreterLog");

let ws = null;
let audioContext = null;
let micStream = null;
let micNode = null;
let playbackTime = 0;
let nextSpeaker = "patient";

const isSpanish = (text) => /[áéíóúüñ¿¡]|\b(el|la|hola|gracias|dolor|estoy|tengo)\b/i.test(text);

function setStatus(text, mode = "live") {
  statusText.textContent = text;
  statusDot.className = `status-dot ${mode}`;
}

function setNextSpeaker(speaker) {
  nextSpeaker = speaker;
  if (!speaker) {
    nextSpeakerEl.textContent = "";
    return;
  }
  const label = speaker === "patient" ? "Patient (Spanish)" : "Doctor (English)";
  nextSpeakerEl.textContent = `Next: ${label}`;
}

function appendBubble(container, text, className, label) {
  const el = document.createElement("div");
  el.className = `bubble ${className}`;
  el.innerHTML = `<span class="bubble-meta">${label}</span>${escapeHtml(text)}`;
  container.appendChild(el);
  container.scrollTop = container.scrollHeight;
}

function escapeHtml(text) {
  return text
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;");
}

function routeTranscript(role, content) {
  if (!content.trim()) return;

  if (role === "user") {
    if (nextSpeaker === "patient" || isSpanish(content)) {
      appendBubble(patientLog, content, "user-es", "Patient said");
    } else {
      appendBubble(doctorLog, content, "user-en", "Doctor said");
    }
    return;
  }

  if (role === "assistant") {
    if (isSpanish(content)) {
      appendBubble(patientLog, content, "assistant-es", "To patient");
      appendBubble(interpreterLog, content, "assistant-es", "Spanish output");
    } else {
      appendBubble(doctorLog, content, "assistant-en", "To doctor");
      appendBubble(interpreterLog, content, "assistant-en", "English output");
    }
  }
}

function floatTo16BitPCM(float32) {
  const buffer = new ArrayBuffer(float32.length * 2);
  const view = new DataView(buffer);
  for (let i = 0; i < float32.length; i++) {
    const sample = Math.max(-1, Math.min(1, float32[i]));
    view.setInt16(i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true);
  }
  return buffer;
}

function resample(input, inputRate, outputRate) {
  if (inputRate === outputRate) return input;
  const ratio = inputRate / outputRate;
  const length = Math.round(input.length / ratio);
  const output = new Float32Array(length);
  for (let i = 0; i < length; i++) {
    const pos = i * ratio;
    const idx = Math.floor(pos);
    const frac = pos - idx;
    const a = input[idx] ?? 0;
    const b = input[idx + 1] ?? a;
    output[i] = a + (b - a) * frac;
  }
  return output;
}

async function startMic(wsUrl) {
  audioContext = new AudioContext();
  micStream = await navigator.mediaDevices.getUserMedia({
    audio: {
      echoCancellation: true,
      noiseSuppression: true,
      channelCount: 1,
    },
  });

  const source = audioContext.createMediaStreamSource(micStream);
  micNode = audioContext.createScriptProcessor(4096, 1, 1);
  const inputRate = audioContext.sampleRate;

  micNode.onaudioprocess = (event) => {
    if (!ws || ws.readyState !== WebSocket.OPEN) return;
    const input = event.inputBuffer.getChannelData(0);
    const resampled = resample(input, inputRate, SAMPLE_RATE);
    ws.send(floatTo16BitPCM(resampled));
  };

  source.connect(micNode);
  micNode.connect(audioContext.destination);
}

function playPcmChunk(arrayBuffer) {
  if (!audioContext) {
    audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
    playbackTime = audioContext.currentTime;
  }

  const int16 = new Int16Array(arrayBuffer);
  const float32 = new Float32Array(int16.length);
  for (let i = 0; i < int16.length; i++) {
    float32[i] = int16[i] / 0x8000;
  }

  const buffer = audioContext.createBuffer(1, float32.length, SAMPLE_RATE);
  buffer.copyToChannel(float32, 0);

  const source = audioContext.createBufferSource();
  source.buffer = buffer;
  source.connect(audioContext.destination);

  const startAt = Math.max(audioContext.currentTime, playbackTime);
  source.start(startAt);
  playbackTime = startAt + buffer.duration;
}

function stopSession() {
  ws?.send(JSON.stringify({ type: "stop" }));
  ws?.close();
  ws = null;

  micNode?.disconnect();
  micNode = null;
  micStream?.getTracks().forEach((track) => track.stop());
  micStream = null;
  audioContext?.close();
  audioContext = null;
  playbackTime = 0;

  startBtn.disabled = false;
  stopBtn.disabled = true;
  setStatus("Session ended", "idle");
  setNextSpeaker(null);
}

function handleEvent(msg) {
  switch (msg.type) {
    case "status":
      setStatus(msg.message, msg.message.includes("Speaking") ? "speaking" : "live");
      break;
    case "nextSpeaker":
      setNextSpeaker(msg.speaker);
      break;
    case "transcript":
      routeTranscript(msg.role, msg.content);
      break;
    case "ready":
      setStatus("Ready — patient may speak", "live");
      setNextSpeaker("patient");
      break;
    case "clearAudio":
      playbackTime = audioContext?.currentTime ?? 0;
      break;
    case "error":
      setStatus(msg.message, "error");
      break;
    default:
      break;
  }
}

async function startSession() {
  patientLog.innerHTML = "";
  doctorLog.innerHTML = "";
  interpreterLog.innerHTML = "";
  setStatus("Connecting…", "live");
  startBtn.disabled = true;
  stopBtn.disabled = false;

  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
  ws = new WebSocket(`${protocol}//${location.host}`);

  ws.binaryType = "arraybuffer";

  ws.addEventListener("open",
[truncated — 575 more characters]
```

### pnpm-workspace.yaml

```yaml
allowBuilds:
  speaker: true

```

### lib/deepgramClient.js

```javascript
import "./network.js";
import { DeepgramClient, DeepgramEnvironment } from "@deepgram/sdk";
import { verifyDeepgramDns } from "./network.js";

export async function createDeepgramClient(apiKey) {
  const resolved = await verifyDeepgramDns();
  console.log(`Resolved ${resolved.address} for agent.deepgram.com`);

  const agentWs =
    process.env.DEEPGRAM_AGENT_URL ?? DeepgramEnvironment.Production.agent;

  return new DeepgramClient({
    apiKey,
    environment: {
      ...DeepgramEnvironment.Production,
      agent: agentWs,
      agentRest:
        process.env.DEEPGRAM_AGENT_REST_URL ??
        DeepgramEnvironment.Production.agentRest,
    },
  });
}

```

### lib/network.js

```javascript
import dns from "node:dns";

/** Prefer IPv4; many networks advertise broken IPv6 DNS records. */
dns.setDefaultResultOrder("ipv4first");

/**
 * Use public DNS resolvers so campus/captive DNS cannot block agent.deepgram.com.
 * Override with DNS_SERVERS= (comma-separated) in .env if needed.
 */
const defaultServers = ["8.8.8.8", "8.8.4.4", "1.1.1.1"];
const envServers = process.env.DNS_SERVERS?.split(",")
  .map((s) => s.trim())
  .filter(Boolean);

dns.setServers(envServers?.length ? envServers : defaultServers);

export const DEEPGRAM_AGENT_HOST = "agent.deepgram.com";

export async function verifyDeepgramDns() {
  const { lookup } = dns.promises;
  return lookup(DEEPGRAM_AGENT_HOST, { family: 4 });
}

```

### scripts/doctor.js

```javascript
import "dotenv/config";
import "../lib/network.js";
import { DEEPGRAM_AGENT_HOST, verifyDeepgramDns } from "../lib/network.js";
import { createDeepgramClient } from "../lib/deepgramClient.js";

console.log("Deepgram network check\n");

try {
  const { address } = await verifyDeepgramDns();
  console.log(`✓ DNS: ${DEEPGRAM_AGENT_HOST} → ${address}`);
} catch (error) {
  console.error(`✗ DNS failed: ${error.message}`);
  console.error(`
Try these fixes:
  1. Phone hotspot (campus Wi-Fi often blocks APIs)
  2. Turn VPN off
  3. Run: nslookup ${DEEPGRAM_AGENT_HOST}
  4. If nslookup works but Node fails, add to /etc/hosts:
       sudo sh -c 'echo "$(nslookup ${DEEPGRAM_AGENT_HOST} | awk "/^Address: / { print \\$2; exit }") ${DEEPGRAM_AGENT_HOST}" >> /etc/hosts'
`);
  process.exit(1);
}

if (!process.env.DEEPGRAM_API_KEY) {
  console.warn("⚠ DEEPGRAM_API_KEY not set in .env (skipping WebSocket test)");
  process.exit(0);
}

try {
  const client = await createDeepgramClient(process.env.DEEPGRAM_API_KEY);
  const agent = await client.agent.v1.connect();
  agent.connect();
  await Promise.race([
    agent.waitForOpen(),
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error("WebSocket timeout")), 10000),
    ),
  ]);
  console.log("✓ WebSocket: connected to Deepgram agent");
  agent.socket.close();
} catch (error) {
  console.error(`✗ WebSocket failed: ${error.message}`);
  process.exit(1);
}

```

### public/index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Medical Interpreter Demo</title>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <main class="app">
      <header class="header">
        <div>
          <p class="eyebrow">Cal AI Hackathon</p>
          <h1>Medical Interpreter</h1>
          <p class="subtitle">
            Real-time English ↔ Spanish interpretation for doctor and patient.
          </p>
        </div>
        <div class="controls">
          <button id="startBtn" class="btn btn-primary">Start session</button>
          <button id="stopBtn" class="btn btn-secondary" disabled>Stop</button>
        </div>
      </header>

      <section class="status-bar">
        <span id="statusDot" class="status-dot idle"></span>
        <span id="statusText">Press Start to begin</span>
        <span id="nextSpeaker" class="next-speaker"></span>
      </section>

      <section class="panels">
        <article class="panel patient">
          <h2>Patient</h2>
          <p class="lang">Speaks Spanish</p>
          <div id="patientLog" class="log"></div>
        </article>

        <article class="panel interpreter">
          <h2>Interpreter</h2>
          <p class="lang">Translates both ways</p>
          <div id="interpreterLog" class="log"></div>
        </article>

        <article class="panel doctor">
          <h2>Doctor</h2>
          <p class="lang">Speaks English</p>
          <div id="doctorLog" class="log"></div>
        </article>
      </section>

      <footer class="footer">
        Use headphones for the best demo. Allow microphone access when prompted.
      </footer>
    </main>
    <script src="/app.js" type="module"></script>
  </body>
</html>

```

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