Inspiration
Clinical skills are learned by talking to patients, but practice is scarce. OSCE-style sessions with standardised patients are expensive, scheduled rarely, and need physical attendance, so many trainees get very little of them. We wanted a place to practise the real loop on demand: take the history, order tests, make the call, and get honest feedback.
What it does
Medicate is a browser-based clinic where you play the doctor.
- A patient is called in and you speak to them in real time. They answer in character, in a voice that fits their age and gender.
- You can examine the patient, order labs, read imaging, diagnose and prescribe.
- When you finish, an AI attending grader (Claude Opus) scores data-gathering, clinical management and interpersonal skills. It cites guidelines from a curated registry instead of inventing sources.
- New in our version: every finished encounter is sent to an n8n workflow, and a mid-encounter "Ask my tutor" button escalates the last few lines of the conversation to that same workflow.
How we built it
Voice pipeline. Browser microphone → LiveKit (WebRTC) → Deepgram Nova-3 speech-to-text → Claude Haiku 4.5 as the patient → ElevenLabs text-to-speech → back to the browser. A single turn's delay is roughly
$$t_{\text{turn}} \approx t_{\text{STT}} + t_{\text{LLM}} + t_{\text{TTS}}$$
so we used streaming at every stage and ElevenLabs' low-latency Flash model.
What we changed or added:
- Swapped Cartesia for the LiveKit ElevenLabs plugin, with one config file (
voices.json) mapping each patient persona (gender × age band: young, adult, older) to an ElevenLabs voice, plus a default. - Built an n8n relay in the FastAPI backend. The browser never sees the webhook URL, and there is a hard 10-second timeout. If n8n fails, the app shows a small toast and carries on.
- Added the "Ask my tutor" escalation and a small toast component.
- Renamed the product to Medicate (user-facing text only), removed all background music, and gave the interface brighter wordmark, heading and button colours without touching game logic.
Stack: React 18, TypeScript, Vite, Three.js, LiveKit, FastAPI, Anthropic Claude (Haiku 4.5 for patients, Opus for grading), Deepgram, ElevenLabs, n8n.
Challenges we ran into
- Running on ARM64 under WSL2. Importing the Anthropic SDK took 30–45 seconds because Python was crawling the Windows drive. Moving the virtual environments onto the Linux filesystem and launching Python with
-Pbrought startup down to about a second. - Two Node installs.
npmin WSL silently picked up the Windows copy and failed with a rollup error until we put the Linux Node first on the path. - API key permissions. The first ElevenLabs key was restricted, and both text-to-speech and voice listing returned
401until the right permissions were turned on. Two voice IDs we'd guessed were also wrong, so we verified every voice against the live account and tested each one. - Keeping secrets server-side. All keys and the n8n URL stay in the backend, and we checked that none of them appear in the built frontend.
- Readable colour. Bright brand colours often failed contrast checks on pastel backgrounds, so we deepened the text shades slightly until large text reached 3:1 and small text 4.5:1.
Accomplishments that we're proud of
- A full spoken conversation loop that works end to end: the patient speaks first, answers in a voice matched to their age and gender, and says goodbye out loud.
- All seven configured voices verified against the live ElevenLabs account, and the voice worker confirmed to speak into a real LiveKit room.
- An n8n integration that can never block training: tested against an unreachable URL, a hanging endpoint (cut off at exactly 10 seconds) and a missing configuration.
- Every key and the webhook URL stay server-side, with a clean check of the built frontend.
- The original data checks (4 of 4) and test suite (18 of 18) still pass after all our changes.
What we learned
- Voice apps are a chain of independent services, and the weakest link sets the experience. Test each link on its own before blaming the whole chain.
- Resilience belongs at the edges. An integration like n8n should never be able to block a training session.
- Developing across Windows and WSL has real performance and tooling pitfalls that are worth solving up front.
- A simple declarative config (persona → voice) beats hard-coded choices, because we could fix and tune voices without touching code.
What's next for Medicate
More cases and specialities, older female voices for the oldest personas, and richer tutor workflows driven by the n8n escalations.
Medicate is a training simulator with synthetic cases. It is not a clinical tool and gives no real medical advice.
