Family calls that name who is speaking, and turn conversations into a family tree.
SocialHTML · TypeScriptGitHub @DarrenChan99OnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Family video calls that tell Grandma who is talking.
When a family crowds around one phone to call someone with memory loss, she hears a stream of voices with no names attached. She can't see who is speaking, and she may not remember whose voice is whose. Most call apps assume one person per device, so they have nothing to say about a room full of voices.
Caretaker is built for nursing-home residents with dementia and the families who call them. It names each voice as it speaks, says how that person is related to her ("your grandson Kevin"), and keeps the good news from the call on her screen after everyone hangs up.
Built for the Congressional App Challenge 2026.
It knows who is speaking. Every two seconds of voice becomes a 192-number voiceprint. Caretaker compares it with the voiceprints of family members who enrolled by reading six short sentences, and lights up the matching person's tile with their name and relationship to her.
It never guesses. If a voice doesn't clearly match one person, the app says "Someone new". A wrong name is worse than no name for someone with dementia: it teaches her the wrong thing about the people she loves. So a name has to pass two tests, a minimum similarity and a clear lead over the second-best match.
It writes down what was said, with names. Whisper speech recognition runs alongside, and each word is filed under the speaker who said it. The family sees a live, named transcript and can fix a wrong label with one tap.
It keeps the good news. A silent notetaker picks out moments worth remembering ("David passed his driving test") and puts them on her Family News screen. Memories publish automatically and the family can remove them afterwards. New people and family-tree changes wait for a family member to confirm them. Medical notes never reach her screen.
It is built for her, not for us. The resident view runs on an iPad on a stand and follows rules enforced in code, not just in comments:
microphone ─▶ find speech ─▶ take 2 s ─▶ map the pitch ─▶ voiceprint ─▶ match a name ─▶ hold it steady ─▶ show it
(Silero VAD) every 1 s (80-band log-Mel) (ECAPA-TDNN) (cosine, τ + margin) (hysteresis)
└──────▶ write the words (Whisper) ─▶ file each word under its speaker ────────────────────────────────▲
| Step | What it does, plainly | Why it matters |
|---|---|---|
| Find speech | A small neural network marks where speech starts and stops | Silence and room noise never become a voiceprint |
| Take 2 seconds | Re-scores the last 2 s of speech once a second | The name appears about 2 s after someone starts talking |
| Map the pitch | Turns the sound into a map of 80 pitch bands over time | It's what the voice model reads |
| Voiceprint | A pretrained ECAPA-TDNN model (20.8 M parameters) turns the clip into 192 numbers | Clips of the same person land close together |
| Match a name | Compares that voiceprint with each family member's average (cosine similarity) | A name only if it clears τ = 0.45 and leads the runner-up by 0.05 |
| Hold it steady | Keeps the current name until a different one wins twice in a row | A cough or a laugh doesn't flip the name |
I did not train the voice model. I run a model already trained on thousands of public, open-source voices (SpeechBrain's ECAPA-TDNN, trained on VoxCeleb). None of my family's audio was used to train it. My recordings only build each person's voiceprint and test the system. My own work is everything around the model: the enrollment recorder, building voiceprints, the open-set decision rule, the smoothing, choosing the threshold from data, the evaluation, and the app.
The /how-it-works page draws this whole pipeline from real recordings: the
spectrogram, the inside of the voice model, a 3-D map of the voiceprints, and the
decision zone on a sphere. /how-it-works/stats shows the accuracy charts.
Measured on my own recordings plus 40 strangers from LibriSpeech (a free library of audiobook recordings). Every test clip was kept out of the voiceprint it was checked against, so the system never saw the answer ahead of time.
| Result | |
|---|---|
| Whole sentences named correctly | 100 % |
| 2-second live slices named correctly | about 96–98 %. The rest show "Someone new", none get a wrong name |
| Stranger slices given a family name | 0 of 240 (40 LibriSpeech voices) |
| Equal error rate | 0.66 % |
| Separation score (ROC AUC) | 0.998 |
| Speed | about 32 ms of work per second of audio, roughly 30× faster than real time on a laptop |
What I learned the hard way:
Limits: few enrolled people (so mixing up two relatives is under-tested), one microphone, quiet rooms, English only. I don't claim more than that.
Each one was chosen from measurements, not intuition. τ and the margin live in
demo.config.json; the rest are in services/speaker/caretaker_speaker/.
| Setting | Value | What it does |
|---|---|---|
| τ (threshold) | 0.45 | Minimum similarity needed to name anyone |
| Margin | 0.05 | How far the best match must beat the runner-up. Below this it says "Someone new" instead of picking between two relatives |
| Window | 2 s, every 1 s | The shortest slice that still scored 96–98 % |
| End of turn | 500 ms of silence | When one person's turn is over |
| Stickiness | 2 windows | A different name has to win twice in a row before the label changes |
| Enrollment | 6 sentences, ~20 s | Accuracy stops improving at about 20 seconds of speech |
The guiding rule: "Someone new" can be fixed with one tap; a wrong name can't be undone in her head. Where the two mistakes compete, the code chooses "Someone new".
apps/web/styles/tokens.css), two typefaces kept apart:
Inter for the family view, Atkinson Hyperlegible for the resident viewapps/web/ the app: family view, resident view, /how-it-works
services/speaker/ voice ID: recorder, enrollment, scoring, streaming loop, replay
ml/eval/ evaluation: LibriSpeech tests, threshold picking, stats for the site
demo.config.json who is enrolled, which family member they play, τ and margin
The app (no setup beyond Node and pnpm):
cd apps/web
pnpm install
pnpm dev # http://localhost:3000
pnpm test # relationship, resident-feed and timeline checks
The call screens replay a recorded call (three family members, one laptop mic),
cut at speaker changes with pauses added, so the demo works without a microphone or a
second person. The speaker-ID pipeline and its accuracy are real and
shown on /how-it-works and /how-it-works/stats. Pause and scrub it from the header
on /family/call.
The voice pipeline (Python 3.12):
cd services/speaker
uv venv --python 3.12 .venv && uv pip install --python .venv/bin/python -e .
.venv/bin/python -m caretaker_speaker.record # local page that coaches each person through 6 sentences
.venv/bin/python -m caretaker_speaker.enroll # one voiceprint per person -> data/voiceprints.npz
.venv/bin/python -m caretaker_speaker.replay call.wav out.json # recorded call -> named timeline for the app
.venv/bin/python -m pytest
The recorder measures the room's noise floor, shows a live level meter, and rejects takes that are too short, too quiet or clipped. Rejected takes go to a separate folder, so they never reach a voiceprint.
Re-measuring:
cd services/speaker
PYTHONPATH=../..:. .venv/bin/python -m ml.eval.run_eval --set librispeech # enrolled vs. stranger trials
PYTHONPATH=../..:. .venv/bin/python -m ml.eval.voice_report # regenerates the /how-it-works/stats data
PYTHONPATH=../..:. .venv/bin/python -m ml.eval.run_call score out.json labels.csv # scores a call against hand labels
Voice recordings and voiceprints stay on the local machine in data/, which is
git-ignored. This repository contains no one's voice. Everyone recorded gave
consent. Speaker ID and Whisper both run locally; nothing is sent to a cloud service.
services/speaker) and the numbers on /how-it-works/stats are real and measured.
Live streaming uses the same scoring code behind a socket and is next.Built by @DarrenChan99 for the Congressional App Challenge 2026.
AI use: I used Claude Code for scaffolding the stack, implmenting my design vision, brainstorming and researching ways to solve challenges (like diarization).