Skip to main contentSkip to 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 identical speech bubbles, one behind the other, and scissors cutting the duplicate away: the same line transcribed twice, fixed by deleting code.
On this page
Reviewed against the shipped implementation and retained tests on October 7, 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". The same audio at the same moment produced two transcriptions that disagreed about what was said, and that disagreement is what made the duplicate hard to filter out.

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 captured the microphone and the system audio, ran a transcription pipeline for each, tagged one set of segments "Speaker N" and the other "Remote N", and translated 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. That is two sockets, two transcriptions and two bills for one utterance. Diarization cannot tell that the two copies are the same speaker, because acoustically they are very different signals.

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 and on our test recordings, and the first real-world screenshot we got back showed it failing and 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, so the ASR model heard different audio and produced different words. Token overlap drops furthest on the sentences where the two paths disagree most (proper nouns, fast speech, crosstalk), and those are also 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 passed the filter untouched. Each constant in that filter was a tuning parameter that could be set wrong, and a threshold in a bugfix usually means the cause is still there and has only been made rarer.

The fix was a delete

Every version of Foxl Notes before v0.4.0 was free of this bug because 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, which cannot restore it.

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. The mixed signal goes over one WebSocket and comes back as 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 was removed, and so were the "Remote N" label space and its collision-avoidance logic, because 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, but it is easy to lose sight of. 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.

A better question to ask 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.

Update, October 2026: Foxl Notes now also transcribes through a service you bring your own key for. To set up AssemblyAI step by step, see Your first note, or the AssemblyAI section of our AirPods post.

References and further reading

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