Inspiration I'm a professor. For years I watched students who clearly knew their material freeze the moment they had to say it out loud — in mock interviews, in panels, defending a thesis, negotiating a starting salary. It was never a knowledge gap. It was the first time anyone had actually pushed back on them, out loud, in real time, with no delete key and no time to think of the perfect answer.

Every "practice" tool I could find was either a static question bank (read the question, silently know the answer, feel prepared, then freeze anyway) or a chatbot that politely waited its turn like a very patient search engine. Neither one trains the thing that actually breaks under pressure: staying coherent while someone interrupts you, doubts your evidence, or corners you with the one follow-up you didn't prepare for.

So I built RehearseAI — an AI that doesn't just ask questions, it pushes back — and tested it extensively with real students, in real practice sessions, before ever putting it in front of a stranger.

What It Does RehearseAI runs realistic, voice-first practice sessions across eleven high-stakes conversation types: job interviews, U.S. visa interviews, salary negotiations, sales pitches, panel discussions, thesis defenses, difficult conversations, presentations, teaching sessions, podcast-style interviews, and open-ended casual chat.

Every session runs on one of several pressure profiles rather than a single fixed difficulty:

Beginner / Guided Reasoning Mode — walks a first-timer through how an answer should be structured (situation → objection → reasoning → evidence → resolution) before pressure is introduced, for people who freeze from unfamiliarity, not lack of skill. Advanced — the default "real" interview: realistic questions, follow-ups, and interruptions. Brutal Mode — maximum pressure, direct and high-friction, but constrained by an explicit safety policy so it never tips into something abusive (see below). Nerve Mode — you upload your own pitch deck, thesis, or proposal, and the AI becomes an adversarial reviewer probing your specific document instead of a generic scenario ("What's your moat if a funded competitor enters tomorrow?", "Your CAC assumption looks optimistic — defend it."). Structured Courses — a generated 30-day program with daily pressure "missions" (e.g. Day 12: recover after repeated interruption) and adaptive difficulty, for people who want to build the skill over time rather than in one session. Every session ends in a structured report rather than a vague "good job": a JSON schema scoring confidence, clarity, persuasiveness, calmness, and structure (0–100 each), plus qualitative breakdowns of strengths, weak moments, missed opportunities, rewritten "improved responses" for your actual weak answers, targeted drills, and a next recommendation — never framed as a guarantee of success, and explicitly not therapy, legal, medical, or financial advice.

The homepage now leads with a real recorded session — an actual RehearseAI interview playing on loop in the hero — instead of asking a visitor to imagine what the product does, plus a "Guaranteed success" badge in the hero and closing CTA that we made sure doesn't quietly contradict the honesty language elsewhere on the site (we rewrote the one FAQ entry that used to hedge against it).

A first-time visitor can go from the homepage to actually talking to the AI with zero signup friction — one age/consent tap, no email, no password — via Firebase anonymous auth. Account creation is deferred to after they've completed a session and want to save the report, when the value is obvious instead of promised. Free, Pro ($19/mo), and Coach ($29/mo) tiers gate session limits and mode access (Brutal and Nerve Mode are paid), billed through Paddle.

How We Built It Frontend: Next.js 16 / React 19, Tailwind, Framer Motion for the interaction layer, deployed as a containerized build (Cloud Build → Google Cloud Run) behind Firestore and Firebase Auth.

Backend: FastAPI, talking to Google's Gemini for the interviewer's reasoning and dialogue generation, with a persistent WebSocket connection per session so turn-taking feels like a conversation instead of a request/response form. Prompts are split by mode (roleplay_prompts.py, panel_prompts.py, analysis_prompts.py, report_prompts.py) rather than one mega-prompt, so a panel discussion's multi-voice dynamic and a one-on-one interview's pressure curve don't have to compromise on the same instructions.

Real-time voice: audio is transcribed through Deepgram at under one second of latency — the same transcription engine used in enterprise call-center tooling — with scoring calibrated so non-native English speakers are judged on the quality of their reasoning, not their accent (40+ languages are supported in text mode).

The genuinely hard problem — knowing when to interrupt. A fixed silence timeout makes an AI interviewer either interrupt mid-thought or sit through ten seconds of dead air waiting for someone who's actually just done. Real conversational timing is personal: some people think out loud with long pauses, others go quiet right before their strongest point. We built an on-device model that learns each user's own pacing from their last 30 turns and continuously re-derives a personal "long pause" threshold:

$$ \text{threshold}{\text{ms}} = \operatorname{clip}\Big(p{65}(\text{silence}) + 1800\cdot r_{\text{premature}} - 900\cdot r_{\text{forced}},\ 1600,\ 6500\Big) $$

where $p_{65}(\text{silence})$ is the 65th percentile of that user's recent finish-silences, $r_{\text{premature}}$ is how often the AI jumped in too early, and $r_{\text{forced}}$ is how often the user hit the hard timeout waiting for it. Two opposing error signals nudging the same number — it only activates after 5 recorded turns, and degrades to a sane fixed default before that, entirely client-side (localStorage, rolling 30-turn window, nothing sent to a server).

We layered a second, optional signal on top: an on-device MediaPipe face-landmark pipeline (running at ~12fps, purely local, no video ever leaves the browser) that approximates gaze stability and blink rate, to help distinguish "eyes up, still thinking" from "actually finished talking" — flagged in the timing data as cameraAssisted so we can separately evaluate whether it actually improves interruption accuracy or is just adding noise. We recently fixed a gap in that instrumentation: the telemetry write path was gating all recording on the camera-assisted-timing setting itself, so turning the camera off also silenced the record that it had been turned off — destroying exactly the on/off comparison the signal exists to validate. Consent now gates recording; the camera setting only gates whether that signal drives the decision, and we log the toggle event itself, not just per-turn flags.

For the harder version of the same problem — classifying pause type directly from voice (thinking / finished / confused / abandoned) — we designed a librosa + scikit-learn classifier pipeline (train_pause_classifier.py, train_user_state_classifier.py) but deliberately shipped it as a scaffold with a rule-based fallback rather than rushing a half-validated model into a product that scores real people: load a trained model only once its metadata clears a minimum evaluation bar, always prefer the heuristic when confidence is low, and never train on raw audio without an explicit consent and retention design.

Staying constructive, not abusive. An AI that's allowed to push back needs a floor. We wrote an explicit safety policy and an age-gate (16+, with guardian-consent language surfaced for anyone who might be under 18) so Brutal Mode stays high-pressure — interrupting, challenging, skeptical — without ever crossing into something that would actually harm someone mid-session.

Account continuity: because most people start as anonymous guests, signing up later has to link onto the same Firebase identity rather than create a fresh one — otherwise the exact session and report someone just cared enough to want to save would get orphaned under an account they never end up using. Now that anonymous trial volume is meaningfully bigger, we also split anonymous guest-trial sessions from real accounts in the admin stats themselves (see below) rather than just at the auth layer.

Findable, not just fast: getting real traffic meant Googlebot started crawling authenticated-only pages (/courses, /history, /notifications, /progress, /subscription, /voice-calibration, /age-check) and hitting the same client-side redirect to /?auth=signin a logged-out human would — which Search Console flagged as both a redirect error and a duplicate-content error on the query-string variant. We blocked those routes in robots.txt, added a metadataBase and homepage canonical so /?auth=signin-style variants canonicalize back to /, and gave /pricing, /try, and /contact their own title and canonical instead of silently inheriting the homepage's.

Analytics: GA4, Meta Pixel, and Microsoft Clarity for session recording, with a custom Firestore-backed event log so funnel counts in the internal admin dashboard are real numbers, not estimates — session start, first AI question shown, first response submitted, interview completed, report viewed, signup started/completed, all tied together by a first-touch UTM-tagged session ID.

Challenges We Faced The hardest bugs we hit weren't in the product — they were in how we were measuring it, twice.

First, our funnel data reported something that looked impossible: real visitors were landing on the homepage, but our custom "landing page viewed" event was firing for roughly 1 in 7 of them, while paid-social traffic showed near-zero engagement across the board — averaging literally 0 seconds and 0.07 events per user. It turned out our own homepage — dozens of concurrently animated hero elements — was heavy enough that on slow or constrained connections (in-app browsers from ad traffic especially) our analytics event, fired from a React useEffect deep in the component tree, would lose the race against the user simply bouncing before hydration finished. GA's own automatic pageview still counted them; our custom funnel event didn't. We were reading a measurement bug as a traffic-quality problem, which would have led us to blame the wrong thing entirely — ad targeting instead of our own render performance.

Second, once zero-friction anonymous trials started generating real volume, the admin dashboard's own mismatch banner started crying wolf: it was diffing total sign-in attempts against completed accounts and reporting a growing pile of "failed signups" that were, in reality, guests who never intended to create an account at all. We split Firebase Auth anonymous sessions out from real accounts at the stats layer, so the dashboard now reports a distinct "Anonymous Guest Sessions" count instead of folding it into a metric that implies something is broken.

Two structurally similar problems ran through the rest of the build:

Interruption timing is a distributed-systems problem wearing a UX costume — silence detection, ASR finality, and network jitter all have to agree fast enough that "the AI cut me off" feels like judgment, not lag. Pressure vs. harm is a product-policy problem disguised as a prompt-engineering one — "brutal but constructive" isn't a personality trait you can prompt your way into once; it needs an explicit, testable policy and an age floor, not vibes. Accomplishments We're Proud Of A signup-optional first session — the AI is talking back to you before you've typed an email address. A personal, self-tuning interruption threshold instead of one global timeout that's wrong for almost everyone. Shipping the ML scaffold with its fallback-first, consent-gated design before shipping a model, instead of the other way around. Catching and fixing two separate self-inflicted measurement bugs — a funnel event that silently died on slow connections, and an admin metric that mistook guest trials for failed signups — before either one quietly cost us weeks of misdirected optimization. Replacing a "trust us" homepage with a real recorded session and closing the loop on our own trust claims, so the "guaranteed success" badge doesn't sit next to an FAQ answer that contradicts it. What We Learned The unglamorous parts mattered more than the flashy ones. An anonymous auth flow that removes a single form field changed more behavior than any prompt tweak. A threshold formula with two opposing error terms beat a fixed timeout nobody was happy with. And you cannot trust your own numbers — funnel or admin — until you've verified the instrumentation itself survives the exact conditions it's actually operating under: slow networks and heavy animation for the funnel event, a growing anonymous-trial population for the admin stats. Building something that watches for the moment a person freezes turned out to require watching, just as carefully, for the moment our own tooling quietly stopped noticing them, or started noticing the wrong thing.

What's Next for RehearseAI Now that camera on/off telemetry is captured correctly end-to-end, actually run the comparison: push camera-assisted timing from an experimental signal to a validated one — or drop it if it doesn't earn its complexity. Train and evaluate the pause/user-state classifiers against the fallback heuristics on real (consented) session data, and only promote a model if it clears the evaluation bar. Expand Panel Discussion mode to multiple distinct AI personas with independent memory and disagreement, rather than one interviewer voice. Deeper post-session drills that regenerate practice turns specifically around a user's worst moment from their last report, instead of generic tips. Built With nextjs react typescript tailwindcss framer-motion fastapi python google-gemini firebase firestore websockets deepgram mediapipe scikit-learn librosa google-cloud-run google-cloud-build paddle google-analytics meta-pixel microsoft-clarity

Built With

Share this project:

Updates