projects
← All projects

Interview Practice

A pipeline for practicing coding interviews out loud: a browser recorder captures audio and every keystroke, transcription and an LLM evaluation turn the session into scored, timestamped feedback, and a review app replays it all in sync.

Spring 2026
  • TypeScript
  • React
  • LLM APIs
  • ffmpeg
  • Nix

Overview

A browser recorder captures audio and every editor keystroke while a coding problem is solved out loud. The recording is transcribed, corrected, and scored by an LLM against a rubric, each observation tied to a timestamp. A review app plays the session back, with code, audio, and transcript locked to one timeline and the evaluation’s key moments as seek points.

Live demo

A real 15.6-minute practice session, reviewable in the actual Playback app: an account-sharing detection problem solved in Python, with the ElevenLabs Scribe transcript and an LLM evaluation loaded alongside the audio and the code timeline.

The session in Playback at 24x, from first keystroke to finished solution. The code pane replays the editor, the transcript tracks the audio underneath, and the dots on the timeline are the evaluation's key moments.

Before pressing play: the audio is an unedited close microphone, and every lip smack survived. Apologies in advance to anyone with misophonia. The page also loads about 21 MB, most of it the recording.

The app is the unmodified production build, and the session, transcript, and evaluation are unedited pipeline artifacts served statically.

The pipeline

replayed in syncsession.jsonaudio + 579 editstranscript.json63 timed segmentsevaluation.json12 scores + 5 key momentsTape Deckrecorderaudio, segmentedevery editor keystrokeruns in the browserRosettatranscriptionsilence strippingchunk and transcribeLLM jargon correctionLensevaluationcode timeline + transcriptversioned rubric promptLLM scores + key momentsPlaybackreviewaudio, code, transcript, and scores on one timeline
One session's path through the pipeline. Each stage writes one JSON artifact; Playback reads all three and replays them on a single timeline.

The three JSON files are the entire interface between the apps: each stage reads the previous artifact and writes its own, their shapes defined once in a shared types package. Each file is shown below in its stage’s section, trimmed from the demo session.

Tape Deck

Tape Deck records the session in the browser: the microphone, and the full contents of the editor at every keystroke. Both are saved incrementally as they arrive, so an interrupted session can be picked back up, and export bundles the whole thing, code timeline and audio, into a single session.json.

Three consecutive events from the demo session, 195 ms apart in total:

{ "timestamp": 299055, "content": "# connections " }
{ "timestamp": 299148, "content": "# connections =" }
{ "timestamp": 299250, "content": "# connections = " }

There are 579 of these in the session. An event is the whole buffer rather than a delta, which is what lets Playback seek anywhere in the recording without replaying the keystrokes to get there.

session.json: the problem, 579 editor states, and the audio
{
  "version": 2,
  "startedAt": "2026-04-05T23:04:27.329Z",
  "durationMs": 935221,
  "language": "python",
  "problem": "# Account Sharing Detection\n\nYou've compiled a list of IP addresses…",
  "codeEvents": [

    {
      "timestamp": 469946,
      "content": "# connections = [(\"1.1.1.1\", \"mike\"), …]\n\n# mike in set(mike, bob) return c"
    },
 579 events, each the full editor contents at one keystroke
  ],
  "audio": {
    "segments": [
      {
        "startedAt": 10,
        "mimeType": "audio/webm;codecs=opus",
        "data": "GkXfowEAAAA…"
      }
    ]
  }
}

The plan is caught mid-keystroke: at 7:50 the editor holds a half-typed planning comment, mike in set(mike, bob) return c, sketching the approach narrated out loud a minute earlier.

Rosetta

Rosetta turns that audio into a timestamped transcript. The speech-to-text itself is a hosted model’s job; Rosetta’s work is everything around it: deciding which audio is worth paying to transcribe, and repairing what the transcriber gets wrong.

The first problem is silence. A coding interview is full of it, and Whisper does not idle through a long quiet stretch; it invents repeats of the last thing it heard. So audio is silence-stripped with ffmpeg before transcription, and the transcriber only ever hears speech:

said once at 0:01, transcribed four timesas recorded$0.093silence stripped first$0.065quiet read, stripped0:002:004:006:008:0010:0012:0014:0015:35285 s of 935 removed before transcription; Whisper cost fell 31%
The demo recording transcribed by Whisper twice in the experiment harness: as recorded, and silence-stripped first. During the quiet read-through of the problem, Whisper transcribes the one sentence spoken at 0:01 four times. Stripping removes the silence, the hallucinations with it, and 31% of the cost; the trade is 23 zero-length boundary segments where clips were cut.

The second problem is jargon. A transcriber has no idea the candidate’s variables are called t1 and find_first_missing, but the session’s own code knows. A correction pass hands an LLM the transcript plus a vocabulary of identifiers scraped from the code timeline:

vocabulary, from the session's code:find_first_missingmidt2t1
Whisper heardafter correction
0:07You're given the API called is stolen, which takes a timestamp…You're given the API called isStolenAtTime, which takes a timestamp…
4:42find first missing of t1 and t2find_first_missing of t1 and t2
5:10mid equals one plus t2 divided by twomid equals t1 plus t2 divided by two
5:25f is stolen midif is_stolen mid
9:42is stolen t1 equals false foris_stolen(t1) equals false for
Real segments from the binary-is-stolen session's run: what Whisper heard, and what the correction pass restored. 'one' becomes t1 because the vocabulary says t1 exists. The demo session's Scribe transcript ran without a correction pass.
transcript.json: timestamped segments and the code vocabulary
{
  "vocabulary": ["account_sharing_detection", "seen",],
  "correctedSegments": [

    {
      "start": 398034,
      "end": 408174,
      "text": "And so in terms of data structures I want to use, I'm thinking of either HashSet or a dictionary."
    },
    {
      "start": 412734,
      "end": 422114,
      "text": "Because we can early exit, we can exit on the IP we're currently looking at, meaning we don't need to store old IPs."
    },

  ],
  "stats": { "totalAudioMs": 934934, "speechMs": 526874, }
}

Lens

Lens grades the session, and most of its work is building the document the grader reads. 579 editor states are too many to hand to a model, so the timeline is compressed to 24 snapshots and 23 diffs (a snapshot every ninety seconds, or sooner when enough lines change), and each diff is interleaved with the speech that overlapped it. The evaluator reads one document with everything on one clock. This is the interval covering the moment traced in the excerpts above, verbatim from the demo evaluation’s prompt:

### Interval 5 -> 6 [6:38 -> 7:13]

Elapsed: 35s | Speech: 24s | Silence: 11s | Words: 48 | Code: +1/-1 lines

```diff
--- snapshot-5	6:38
+++ snapshot-6	7:13
@@ -1,3 +1,3 @@
 # connections = [("1.1.1.1", "mike"), ("1.1.1.1", "mike")]
 
-#
+# h
```

**Overlapping speech:**

[6:38 -> 6:48] And so in terms of data structures I want to use, I'm thinking of either HashSet or a dictionary.

[6:52 -> 7:02] Because we can early exit, we can exit on the IP we're currently looking at, meaning we don't need to store old IPs.

[7:08 -> 7:13] So a HashSet would be fine.

One of 23 such intervals; the full prompt runs 31,236 characters, about 7,800 tokens, for the 15:35 session. The rubric that follows it is versioned, seven versions in so far. Back comes evaluation.json:

evaluation.json: scored dimensions and timestamped key moments
{
  "promptVersion": "v1",
  "summary": "The candidate demonstrated an incredibly methodical…",
  "dimensions": [

    {
      "dimension": "approach_narration",
      "score": 5,
      "evidence": "At 06:38, they outline the approach: \"in terms of data structures I want to use, I'm thinking of either HashSet or a dictionary.\" They then justify the HashSet at 06:52…"
    },

  ],
  "keyMoments": [

    {
      "timestampMs": 412000,
      "valence": "positive",
      "description": "Candidate accurately justifies why a HashSet rather than a dictionary is sufficient for early exit logic."
    },

  ]
}

The evidence quotes the transcript segments above, and the key moment points back at 6:52; clicking it in Playback jumps there.

A transcript is frozen once written, so evaluations re-run cheaply against it, and the two demo links above are exactly that: two graders given the same session and the same transcript.

Gemini 3.1 ProClaude Opus 4.6not scored12345problem restatementapproach narrationcode and speech alignmentsilence managementdecompositionexample usagedebugging strategyedge case awarenesscorrectnessoptimality awarenesstime allocationrecovery from mistakes
The demo's two evaluations, dimension by dimension (rubric v1, scale 1 to 5). The graders agree on every 5, differ by a point in the middle, and split on whether decomposition was observable at all.
Scores as a table
dimensionGemini 3.1 ProClaude Opus 4.6
problem restatement54
approach narration55
code and speech alignment55
silence management43
decompositionnot scored3
example usage55
debugging strategynot scorednot scored
edge case awareness43
correctness55
optimality awareness55
time allocation44
recovery from mistakesnot scorednot scored

Playback

Playback, a React app, puts it all back together. The audio is reassembled from the recorded segments; the code pane and the transcript follow the same clock, the editor shown exactly as it stood at the playhead. The evaluation sits alongside, and clicking a key moment jumps everything to that instant. It is the app embedded in the live demo above, byte for byte.

The four apps share one TypeScript monorepo and the types the three JSON files obey. An experiment harness re-runs the pipeline under different configurations (transcription model, correction model, prompt version, and evaluator model), recording each run’s configuration and the git SHA of the pipeline code that produced it; the repository holds 126 such runs.