# How OpenMAIC Works Around Browser TTS Audio Bugs: Chrome Speech Synthesis Fixes

> OpenMAIC fixes Chrome TTS audio bugs by canceling utterances and syncing results, preventing desynchronization. Learn how this speech synthesis workaround works.

- Repository: [MAIC/OpenMAIC](https://github.com/THU-MAIC/OpenMAIC)
- Tags: how-to-guide
- Published: 2026-09-05

---

**OpenMAIC eliminates Chrome's speech synthesis "sticking" bug by explicitly canceling active utterances before starting new ones and immediately synchronizing TTS generation results into live scenes to prevent audio metadata desynchronization.**

OpenMAIC runs browser-based text-to-speech pipelines that must contend with a known Chrome bug where previously started synthesis requests can interfere with subsequent playback. The platform implements defensive workarounds in [`lib/playback/engine.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts) and [`lib/workbench/tts-stage-sync.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/workbench/tts-stage-sync.ts) to ensure reliable TTS delivery without audio glitches.

## The Chrome Speech Synthesis Sticking Bug

Browser-based TTS in Chrome suffers from a bug where a previously-started **SpeechSynthesisUtterance** can remain "stuck" in the synthesis queue, corrupting or blocking subsequent speech requests. This manifests when rapid or overlapping TTS calls create stale synthesis states that prevent new audio from starting cleanly. OpenMAIC addresses this through a two-layer defense strategy targeting both playback initialization and scene state management.

## Cancel-Then-Speak Pattern in the Playback Engine

OpenMAIC's primary defense is a **cancel-then-speak** pattern that clears any lingering synthesis state before initiating new utterances. This workaround ensures that Chrome's internal speech synthesis queue never retains stale references that could contaminate the next playback request.

### Implementation in lib/playback/engine.ts

In the playback engine, the `playSpeech` function wraps the Web Speech API with a defensive cancellation step. According to the source code comment in [`lib/playback/engine.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts), this "Chrome bug workaround" ensures that `cancel()` is invoked immediately before `speak()`.

```typescript
// lib/playback/engine.ts (excerpt)
function playSpeech(utterance: SpeechSynthesisUtterance) {
  // Chrome bug workaround: cancel any existing utterance first
  window.speechSynthesis.cancel();
  window.speechSynthesis.speak(utterance);
}

```

This explicit cancellation clears the browser's internal synthesis queue, preventing the "sticking" behavior that would otherwise corrupt the new utterance.

## Live Scene Synchronization for TTS Results

Beyond playback fixes, OpenMAIC ensures the UI reflects corrected audio immediately through a dedicated synchronization routine that folds `generate_tts` results back into the active scene. This prevents the interface from displaying stale action arrays while waiting for a full checkpoint refresh.

### The applyGenerateTtsResultToScenes Function

Located in [`lib/workbench/tts-stage-sync.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/workbench/tts-stage-sync.ts), the `applyGenerateTtsResultToScenes` function intercepts successful TTS generation events and updates the live scene copy. This guarantees that the **Workbench** UI immediately reflects the corrected audio metadata without waiting for background sync processes.

```typescript
// lib/workbench/tts-stage-sync.ts
export function applyGenerateTtsResultToScenes(
  scenes: readonly Scene[],
  event: WorkbenchEvent,
): readonly Scene[] {
  if (event.type !== 'tool_execution_end') return scenes;
  const data = (event.data ?? {}) as Record<string, unknown>;
  if (data.toolName !== 'generate_tts' || data.isError === true) return scenes;
  const result = data.result as { details?: GenerateTtsDetails } | undefined;
  const details = result?.details;
  if (typeof details?.sceneId !== 'string' || !Array.isArray(details.sceneActions))
    return scenes;
  const sceneIndex = scenes.findIndex((scene) => scene.id === details.sceneId);
  if (sceneIndex < 0) return scenes;
  const next = [...scenes];
  next[sceneIndex] = { ...next[sceneIndex], actions: details.sceneActions as Action[] };
  return next;
}

```

The function validates that the event represents a successful **tool_execution_end** for the `generate_tts` tool, then extracts the `sceneId` and updated `sceneActions` to patch the scene array immutably.

### Handling TTS Generation Completion

When the `generate_tts` tool finishes, the result—including the exact array of actions containing newly generated audio IDs—passes through this synchronization layer. The code checks `data.isError` to ensure only successful completions trigger updates, preventing partial or failed TTS results from corrupting the scene state. The presentation layer in [`components/workbench/chat/tool-presentation.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/components/workbench/chat/tool-presentation.ts) then renders the synchronized audio data in the chat interface.

## Summary

- **Cancel-then-speak pattern**: OpenMAIC calls `window.speechSynthesis.cancel()` immediately before `speak()` in [`lib/playback/engine.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts) to clear Chrome's stale synthesis state.
- **Live scene synchronization**: The `applyGenerateTtsResultToScenes` function in [`lib/workbench/tts-stage-sync.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/workbench/tts-stage-sync.ts) patches TTS results directly into live scenes upon successful `generate_tts` completion.
- **Event-driven updates**: The workaround relies on `tool_execution_end` events to trigger UI synchronization without waiting for checkpoint refreshes.
- **Defensive validation**: Both workarounds include guard clauses to prevent error states and ensure only valid TTS data affects the playback and display.

## Frequently Asked Questions

### What causes the Chrome TTS audio bug in OpenMAIC?

The Chrome browser maintains an internal queue for **SpeechSynthesisUtterance** objects that can "stick" or fail to clear properly when synthesis requests overlap or execute rapidly. This leaves stale synthesis states active, causing subsequent `speak()` calls to fail or produce corrupted audio output.

### How does the cancel() method fix speech synthesis sticking?

Calling `window.speechSynthesis.cancel()` forcibly clears the browser's internal synthesis queue and aborts any active utterances. In [`lib/playback/engine.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts), invoking this method immediately before `speak()` ensures a clean slate for each new TTS request, eliminating interference from previous synthesis attempts.

### Why is scene synchronization necessary after generate_tts?

The **Workbench** UI displays scene actions that include audio metadata. Without immediate synchronization via `applyGenerateTtsResultToScenes`, the interface would display stale action arrays until the next checkpoint sync, creating a lag between TTS generation and visual feedback. This function bridges the gap by patching results in real-time.

### Where are the TTS workarounds implemented in the OpenMAIC codebase?

The playback workaround lives in [`lib/playback/engine.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/playback/engine.ts) within the speech playback functions, while the synchronization logic resides in [`lib/workbench/tts-stage-sync.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/lib/workbench/tts-stage-sync.ts). The test suite in [`tests/workbench/tts-stage-sync.test.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/tests/workbench/tts-stage-sync.test.ts) validates the scene synchronization behavior, and [`components/workbench/chat/tool-presentation.ts`](https://github.com/THU-MAIC/OpenMAIC/blob/main/components/workbench/chat/tool-presentation.ts) handles the UI presentation layer for TTS-generated audio.