Skip to main content
Blog
IncidentAudio capture / Streaming transcription / Web Audio

Your Meeting Was Transcribed Twice. Deleting Code Fixed It.

Two independent transcription pipelines appended two rows for the same utterance when the microphone re-heard speaker output. Mixing both captures into one ASR stream removed the two-writer failure mode and let us delete the similarity filter.

Foxl TeamUpdated 6 min read

Two independent audio streams producing duplicate transcripts before being mixed into one signal and one transcription stream.
On this page
Reviewed against the shipped implementation and retained tests on July 16, 2026.

A user sent us a screenshot last week. Their meeting transcript showed the same sentence twice, back to back, attributed to two different people:

Speaker 1
to stay to make a difference. It was part of the
first ever one city day.

Speaker 2
to stay to make a difference. It was part of the
first ever one city day.

Both copies got translated. Both counted against their credits. And if you looked closely, the copies were not quite identical - one had heard "Beerba Buena Gardens", the other "A Yerba Buena Gardens". Same audio, same moment, two transcriptions that disagreed about what was said. That last detail turned out to be the whole story.

Where the second copy came from

Foxl Notes transcribes meetings through Amazon Transcribe, streamed over a WebSocket the foxl.ai relay presigns. The recording integration report traces that path from desktop capture through the relay to Transcribe. Until v0.4.0, live-translate mode captured exactly one audio source: the meeting's system audio. That had an obvious gap - your own questions never got translated, because your voice does not come out of your own speakers. So in v0.4.0 we did the obvious thing. Capture the microphone and the system audio, run a transcription pipeline for each, tag one set of segments "Speaker N" and the other "Remote N", translate everything.

On headphones this works fine. The mic hears you, the system-audio capture hears them, clean separation. On speakers - which is how a lot of people actually sit through meetings - the mic hears you and the room, and the room includes your speakers playing the other person's voice. Every remote utterance now reaches Amazon Transcribe twice: once through the digital capture, once through the air, a desk, and a laptop microphone. Two sockets, two transcriptions, two bills. Diarization has no idea these are the same person; acoustically they barely are.

The filter that almost worked

Our first fix, in v0.4.2, was the one most codebases reach for: detect the duplicates and drop one. Normalize both texts, tokenize, and if a finalizing mic segment shares at least 80% of its tokens with a nearby system-audio segment - nearby meaning within a 12-second window - treat it as speaker echo and delete it.

It worked on our desks. It survived our test recordings. And the very first real-world screenshot we got back showed it failing, and showed exactly why it had to fail. Look at the pair again: "Beerba Buena Gardens" versus "A Yerba Buena Gardens". The two acoustic paths are not copies of each other. One went through a speaker cone, three feet of air, and echo cancellation before hitting a laptop mic; the ASR model heard genuinely different audio and produced genuinely different words. Token overlap collapses precisely on the sentences where the two paths disagree most - proper nouns, fast speech, crosstalk - which are precisely the sentences where transcription is hardest. The filter was calibrated on the easy case and deployed against the hard one.

The 12-second window had the same problem in miniature. The two pipelines kept independent clocks - each one's timestamps anchored to its own socket's first audio frame - so the "same moment" in pipeline A and pipeline B could drift apart, and a duplicate arriving outside the window sailed straight through. Every constant in that filter was a knob, and every knob was a place to be wrong.

A threshold in a bugfix is a confession that you have not found the bug. You have found a price for it.

The fix was a delete

The version of Foxl Notes that never had this bug - everything before v0.4.0 - was not smarter than the code that followed it. It had only one transcription result stream, so two independent pipelines could not emit two rows for the same utterance. In v0.4.0 we traded that invariant for a probability, and in v0.4.2 we tried to buy the invariant back with string comparison. It does not work like that.

v0.4.3 goes back to one result stream without giving up the feature. The Web Audio API makes the topology small: connect two MediaStreamAudioSourceNode sources - the mic and the system audio - to the same processor node, and the browser sums the samples. One mixed signal, one WebSocket, one transcription result stream. The old failure mode, where two independent ASR sessions each append a row, no longer exists.

const sources = streams.map(s => ctx.createMediaStreamSource(s));
const processor = ctx.createScriptProcessor(4096, 1, 1);
sources.forEach(s => s.connect(processor)); // WebAudio sums them

The media-capture echo-cancellation constraint on the mic still matters. It suppresses speaker playback in the mic leg, so the mix favors the clean digital path instead of combining it with a delayed room copy. A failed AEC can still produce echo, muddy audio, or repeated words inside one ASR result. What it cannot recreate is the specific two-writer failure where separate sockets each append their own transcript row.

The knock-on effects were all deletions. The similarity filter: gone. The "Remote N" label space and its collision-avoidance logic: gone - one stream means one diarization space, and Amazon Transcribe's speaker diarization does the job we were doing by hand. The saved audio recording, which had been mic-only and lost the remote party entirely on headphones, now records the same mix the transcriber hears. The commit that fixed a bug two releases of code had failed to fix removed more lines than it added.

What we are taking from it

None of this is a new lesson, which is exactly what makes it worth writing down. When a bug appears after a feature ships, the instinct is to keep the feature's shape and add a defense - a filter, a validator, a reconciliation pass. Sometimes that is right. But defenses inherit the failure modes of the thing they defend, plus their own. A dedup filter for two ASR streams has to be at least as good at deciding "same utterance" as the ASR is at transcribing - and if the ASR were that reliable, the two streams would agree and the filter would be trivial. We were asking the patch to be smarter than the system it patched.

The better question is: what would make this class of failure impossible instead of handled? Sometimes the answer is expensive. Here, changing the stream topology let us delete the cross-pipeline reconciliation code.

References and further reading

  1. Record and transcribe with Foxl NotesDocumentation
  2. Foxl Notes credits and privacyDocumentation