Project Info
Inspiration
During Hurricane Harvey in 2017, Houston's 911 system received over 56,000 calls in a single day, which is more than double its normal volume. Operators were overwhelmed. And that was in English. For the tens of thousands of Houston residents who speak Spanish, Vietnamese, Hindi, or Arabic as their primary language, the barrier wasn't just the flood. It was the phone call itself. Relief organizations running mass-care operations face a version of this problem every time a major disaster hits: thousands of simultaneous calls, dozens of languages, and no free human to triage each one. Translation lines exist, but they add minutes per call — minutes that matter when someone hasn't had water in two days. We built RELAY because the problem isn't a lack of resources. It's a routing problem. The shelter exists. The water station is 1.2km away. The gap is connecting the person in crisis to the resource in real time, in their language, without a human bottleneck.
What it does
RELAY is a multilingual voice intake and routing agent for disaster-relief organizations. A caller in crisis speaks in their own language. RELAY: Listens — transcribes the call in real time using Deepgram streaming STT, with support for multiple languages through automatic language detection Understands — Claude reads the transcript and extracts structured triage data: number of people, injuries, location, and needs. If critical information is missing, RELAY asks a follow-up question in the caller's language rather than triaging on incomplete data Matches — a deterministic routing engine filters real relief resources by availability and capability, ranks by distance, and produces a dispatch Shows — a live map surfaces the caller's location, nearby resources, and a routed line to the best match, color-coded by priority (P1/P2/P3) RELAY is not a 911 replacement. It is the multilingual intake layer for the moment the formal system is overwhelmed — when volume and language barriers are too high for humans to triage alone.
How we built it
The pipeline has four stages, each owned by one team member and built against locked data contracts so all four could develop in parallel: The Ear (Deepgram) — a Node.js WebSocket server captures browser mic audio, downsamples it to 16-bit PCM at 16kHz, and streams it to Deepgram's streaming STT API. Deepgram handles multilingual transcription, interim results (live "typing" effect), and turn-taking via utterance_end events. A confidence threshold of $c \geq 0.6$ gates whether a transcript is sent forward or triggers a re-prompt. After $N = 2$ failed attempts, the system escalates to a human operator. The Brain (Claude / Anthropic) — on each final utterance, the transcript is sent to Claude with a strict system prompt that returns only a structured Triage JSON object. Claude extracts people count, injuries, location (as stated by the caller, never device GPS), needs, and priority. Required slots are: location, people, needs. If any are missing, Claude sets nextQuestion and readyToRoute: false. The Brain never decides the match — it only extracts facts. The Matchmaker — a pure TypeScript function: $$\text{score}(r) = \text{distanceKm}(\text{caller}, r) \quad \text{subject to: } r.\text{has} \cap \text{needs} \neq \emptyset \text{ and } r.\text{availableCapacity} > 0$$ Candidates are filtered by capability and availability, then ranked by distance using the Haversine formula. The top match and two alternatives are returned as a Dispatch object. If nothing fits, matched is null — the function never fabricates a resource. The Map (Mapbox GL JS) — a Next.js frontend renders the caller pin, resource pins, and an animated routing line to the matched resource. The triage card shows the P1/P2/P3 priority chip, dispatch text, and an escalation banner for "911" or "human" cases. Stack: Next.js (App Router) + TypeScript + Tailwind · Node.js WebSocket server · Deepgram Streaming STT · Anthropic Claude API · Mapbox GL JS + Geocoding API · Redis (session memory)
Challenges we ran into
WebSocket audio streaming was the riskiest piece of plumbing in the project. Getting the mic audio from the browser into a format Deepgram accepts — 16-bit PCM, mono, 16kHz — required a custom AudioWorklet (pcm-worklet.js) to downsample from the browser's native sample rate. A subtle buffering bug caused dropped audio frames that looked like low-confidence transcripts, which took several hours to isolate. Keeping Claude grounded took significant prompt engineering. Early versions of the Brain occasionally invented a location or returned prose mixed into the JSON. The fix was a combination of strict output schema enforcement in the prompt, a JSON parse validation layer, and a fallback triage that escalates to a human rather than guessing when the parse fails. Parallel development across four people meant the data contracts in types/index.ts were locked before a line of UI or audio code was written. Even then, Person B and Person C had subtle type mismatches in the Triage.location shape and the Dispatch.matched nullability that only surfaced at the chain-it integration session. We resolved them by treating B's brain/src/types.ts as the source of truth for Triage and C's types/index.ts as the source of truth for Dispatch and Resource. Git merge conflicts in package.json and package-lock.json appeared on nearly every branch merge because each sub-project (brain/, server/, root) added its own dependencies. The reliable fix was deleting package-lock.json and running npm install fresh after each merge rather than hand-resolving the lock file. Live voice in a noisy room remains the demo's biggest risk. We built and rehearsed a full backup audio clip path through the same Deepgram pipeline so the fallback is indistinguishable from the live run.
Accomplishments we're proud of
The Matchmaker is fully testable without AI. Because routing is deterministic TypeScript with no Claude dependency, we could prove the guardrails work — especially the matched: null case — with plain unit tests before a single line of audio or UI code existed. Judges can read the code and see exactly why it can never fabricate a resource. The follow-up question loop. When the Brain detects a missing required slot, RELAY asks one calm, targeted follow-up in the caller's language rather than routing on incomplete information. This is the single beat that makes RELAY an agent rather than a transcriber — and it was deliberately scripted into the hero call so judges see it in the demo. Four people, four parallel workstreams, zero blocking. By locking the data contracts on day one and having each person build against typed stubs rather than each other's actual code, we reached the chain-it integration session with four independently working stages. The integration bugs that surfaced were in the contracts, not the implementations — which is exactly what you want. 18 real Houston relief resources with real addresses, coordinates, and phone numbers — scoped to the Texas Medical Center cluster, downtown shelters, and Red Cross distribution points. The dataset is small enough to be fast and honest enough to be credible.
What we learned
The moat is in the routing, not the translation. Early in the project we spent energy on the voice pipeline. The thing judges respond to is the moment the map lights up — when chaos becomes a routed dispatch. The translation is what gets us in the door; the Matchmaker is the product. Structured output discipline is harder than it looks. Getting an LLM to return only valid JSON, every time, with no markdown fences and no prose, requires more than one instruction in the prompt. It requires schema validation, a fallback path, and a test suite that runs against the real API — not a mock. Escalation is a feature, not a failure. The system's willingness to say "I don't know enough to route this — routing to a human" is the thing that makes it trustworthy in a crisis context. We learned to treat every escalation path as a product decision, not an error case. Git discipline under time pressure saves hours. The practice of one branch per person, merge into an integration branch first, test before merging to main — this slowed down the first merge and saved us on every subsequent one.
What's next
Real capacity feeds. The current dataset is a realistic mock. FEMA's National Shelter System (NSS) publishes real shelter data during activations. Connecting RELAY to a live NSS feed would turn the mock dataset into ground truth. Phone intake via Twilio. The demo uses a browser mic to keep the stack simple. In real deployment, callers need a phone number — a Twilio IVR that pipes audio into the same Deepgram WebSocket. This is the roadmap item closest to production-ready. The urgency classifier. A TF-IDF + Logistic Regression classifier trained on synthetic labeled transcripts would replace Claude's priority field with a measured, auditable score. The story: "it never confuses a P1 for a P3," demonstrated with a confusion matrix. This is the "real ML, not a wrapper" credibility layer. Human-operator dashboard. The current UI surfaces escalations but doesn't give a human operator a full intake view. A dispatcher panel — showing all active calls, their priority, and one-click confirmation — is the next UI milestone.
RELAY
Built at Berkeley AI Hackathon 2026.
RELAY is an operator-facing disaster-relief intake dashboard. A caller speaks in their own language (demo: Hindi) → speech is transcribed live → Claude extracts a structured triage (who, where, how many, what's needed, how urgent) → a deterministic matchmaker routes them to the nearest resource with capacity → a dispatcher watches it all resolve on a map in real time.
LISTEN → UNDERSTAND → MATCH → SHOW
(Ear) (Brain) (Matchmaker) (Map)
No AI invents a resource or a dispatch decision — matching is deterministic
TypeScript over real data; Claude only reads the transcript and fills in the
triage. See docs/DESIGN_BRIEF.md for the full product
and design rationale.
Architecture
RELAY is five independent services plus a shared Redis instance. Each can run standalone; wired together they form the full pipeline.
| Service | Dir | Role | Port |
|---|---|---|---|
| Ear | server/ | Mic capture (browser) → WebSocket → Deepgram streaming STT | 8080 |
| Brain | brain/ | Transcript → structured Triage JSON via Claude, session memory in Redis | 4001 |
| Matchmaker | matchmaker/ | Triage → matched resource via Redis vector search + deterministic ranking | 3002 |
| Classifier (stretch) | classifier/ | TF-IDF + LogisticRegression urgency check Brain consults for a second opinion | 8000 |
| Dashboard | src/ (Next.js) | Operator UI + /api/* proxies to Brain/Matchmaker + Deepgram TTS | 3000 |
| Live UI | frontend/ | Figma-designed dashboard, wired end-to-end to the real backend | Vite default |
| Redis Stack | docker-compose.yml | Brain session memory (TTL) + Matchmaker vector index | 6379 |
Two UIs exist because of how the team split up work: src/ is the
original Next.js dashboard (also demoable standalone with mock data, no
backend needed), and frontend/ is a Figma-designed rebuild wired
to the live pipeline — this is the one used for the demo. Both call the same
/api/triage, /api/dispatch, and /api/speak routes served by the Next.js
app in src/, so that app (or at least its API routes) needs to be running
either way.
Quick start
Demo mode (no backend, mock data):
npm install
npm run dev
# → http://localhost:3000
Full pipeline (live mic → Claude → dispatch): see
docs/SETUP.md for env vars, Redis, and per-service startup
— or bring every service up together:
npm run dev:all # web + brain + ear, concurrently
then in separate shells: cd matchmaker && npm run dev, cd frontend && npm run dev,
and optionally cd classifier && python server.py.
Repo layout
relay/
├── src/ Next.js dashboard (UI + /api/* proxy routes)
├── frontend/ Figma-designed live UI (Vite + React), wired to the real backend
├── server/ Ear — audio capture, WS server, Deepgram STT
├── brain/ Brain — Claude triage, Redis session memory
├── matchmaker/ Matchmaker — Redis vector search + resource ranking
├── classifier/ Urgency classifier (Python/FastAPI, stretch goal)
├── data/ Shared demo resource dataset
├── docker-compose.yml Redis Stack (session memory + vector search)
├── docs/ Setup guide + design brief
├── AGENTS.md / CLAUDE.md Agent-facing project instructions
└── .claude/ Claude Code launch config
Docs
docs/SETUP.md— environment variables, Redis, running each service, wiring the live backend, demo rehearsal checklist.docs/DESIGN_BRIEF.md— full visual design spec (color system, layout, component states, animations).- Each service has its own README with the details specific to it:
server/README.md,classifier/README.md,brain/HERO_CALL.md(the scripted demo call).
Stack
Next.js 15 · React 19 · TypeScript · Tailwind · Mapbox GL JS (dashboard) / Leaflet (live UI) · Express · Redis Stack (RediSearch vector index) · Claude (Anthropic SDK) · Deepgram (streaming STT + Aura TTS) · Python / FastAPI + scikit-learn (classifier).
Analysis
View
Metric
- 6
- 6
- 2
- 1
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- AnthropicIn code
- CSSIn code
- ExpressIn code
- FastAPIIn code
- HTMLIn code
- JavaScriptIn code
- Next.jsIn code
- PythonIn code
- ReactIn code
- Tailwind CSSIn code
- TypeScriptIn code
- Node.jsClaimed
- RedisClaimed
11 of 13 appear in the indexed code. 2 claimed on Devpost could not be matched to code, which may simply mean the tool leaves no trace in the repository.
AI coding agents
- Claude CodeConfig · Commits
- CodexConfig
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
465 KB
Source files
122
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
nysh9/Relay
164 files · 4.9 MB · @ 9b3b312
Structure
Interface
66 files · 40%Screens, components and styles rendered to the user.
API & routing
14 files · 9%Request entry points: routes, handlers and controllers.
Application logic
28 files · 17%Domain rules, services and shared utilities.
+7 more
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- TypeScript76%
- Markdown16%
- Python4%
- CSS2%
- JavaScript1%
- HTML0%
- Other (1)0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
frontend/package.json
npm · 64- @emotion/react
- @emotion/styled
- @mui/icons-material
- @mui/material
- @popperjs/core
- @radix-ui/react-accordion
- @radix-ui/react-alert-dialog
- @radix-ui/react-aspect-ratio
- @radix-ui/react-avatar
- @radix-ui/react-checkbox
- @radix-ui/react-collapsible
- @radix-ui/react-context-menu
- @radix-ui/react-dialog
- @radix-ui/react-dropdown-menu
- @radix-ui/react-hover-card
- @radix-ui/react-label
- @radix-ui/react-menubar
- @radix-ui/react-navigation-menu
- +46 more
package.json
npm · 17- clsx
- mapbox-gl
- next
- react
- react-dom
- +12 more
brain/package.json
npm · 10- @anthropic-ai/sdk
- dotenv
- express
- ioredis
- uuid
- +5 more
classifier/requirements.txt
pypi · 9- anthropic
- fastapi
- joblib
- matplotlib
- numpy
- pandas
- pydantic
- scikit-learn
- uvicorn
server/package.json
npm · 8- @deepgram/sdk
- dotenv
- ws
- +5 more
matchmaker/package.json
npm · 7- @xenova/transformers
- express
- ioredis
- +4 more
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.