Late on July 13, TransLee could hear a quiet news broadcast through my phone’s own speaker and translate it live. By the next morning, that exact feature was silent. Nothing had touched the speaker-recognition code overnight. What had changed was a different feature entirely: translated voice playback had just started working for the first time.

The Symptoms, in Order

  • 03:40 — Speaker-sound recognition, which worked the night before, stopped picking up anything.
  • 04:36 — Still broken. YouTube audio wasn’t recognized at all, and even the transcript-only fallback wasn’t translating, just showing raw text.
  • 07:41 — Fixed the transcript-and-translation display. Recognition worked again. But the voice-playback feature, the one that had worked the previous evening, was now silent.
  • 07:46 to 08:13 — Every attempt to get both working at once produced the same shape of failure: one sentence would translate and play correctly, then everything stopped, no more translation, no more transcript, sometimes a garbled half-second of audio right before it died.

Five hours of this, with the failure mode staying identical no matter which side I patched. That repetition was the actual clue. This wasn’t a bug hiding somewhere in the code. It was two features fighting over the same microphone.

Why They Were Actually Incompatible

The mechanism, once we found it, was simple enough to explain in two sentences.

Speaker mode needs acoustic echo cancellation turned off. Android’s echo cancellation is built to filter out sound the device itself is producing, so a phone doesn’t hear its own voice call audio as if it were new input. Turn it on while trying to listen to the phone’s own speaker, and Android does exactly its job: it cancels out the YouTube sound as an echo, and the microphone has nothing left to hear.

Conversation mode needs it turned on. When TransLee speaks a translated sentence out loud, that sound comes out of the same phone. With echo cancellation off, the tail of that spoken audio loops right back into the microphone, and the model reads it as the human speaker still talking, waiting for a pause that never fully arrives, then giving up after one sentence.

Both settings are correct. They’re just correct for opposite situations, and a browser’s microphone stream can only be opened with one of them, decided the moment getUserMedia() is called. There’s no way to flip it mid-stream. Fixing one mode’s need for a setting always meant breaking the other’s.

💬 Prompt that worked “I think phone-speaker recognition and voice playback can’t work together. They’re both important features, but they’re hard to combine. What if we made this a selectable option instead? If someone picks the speaker-listening mode, just turn off translated voice playback.”

That sentence ended five hours of trying to patch around a conflict that wasn’t a bug at all. The fix stopped being “make both work at once” and became “let the person using the app say which one they need right now.”

What Actually Shipped

A usage-mode selector with two options: Speaker Sound Translation, the default, and Conversation Interpretation. The translated-voice-playback checkbox only appears in conversation mode, since it’s meaningless in the other one. startMic() decides which echo cancellation setting to use based on the selected mode, and if the mode changes while the app is already running, it restarts the microphone stream, since the setting can’t be changed on a stream that’s already open.

One more small piece, added after the main fix: a two-second forced release on the half-duplex gate that blocks the microphone while translated audio is playing, timed against the browser’s own clock rather than assumed. It’s a safety net, not a full solution, if the audio timing drifts for any reason, the microphone unlocks after two seconds no matter what, instead of staying blocked indefinitely. I’m noting that honestly because it’s a guard against an edge case, not proof the timing problem is fully solved.

🗂 Claude.md Rule Usage mode and echo cancellation must always move together. Speaker-sound mode needs echo cancellation off; conversation mode needs it on. The two features cannot both run on one device at the same time, so split them with a usage-mode selector, decide the echo-cancellation setting per mode inside startMic(), and restart the microphone whenever the mode changes while the app is running, since the setting is fixed at the moment getUserMedia() is called.

By 09:08 that morning, both modes worked reliably enough to commit as version 1.0. The whole cycle, from the first regression to a working fix, ran about five and a half hours across one morning, most of it spent assuming the two features could somehow be reconciled before accepting that they simply couldn’t, and that the fix was a choice, not a patch.