# Streaming Speech Panel

Streaming Speech Panel [#streaming-speech-panel]

**Streaming Speech Panel** visualizes a streaming text-to-speech run. While synthesis or playback is active it shows a waveform + spinner, a synthesizing/playing label, the processed-clause count, and the current clause in a highlighted "now playing" box. When the stream completes it shows the clause-count summary, a "generated locally" privacy note, and a Download WAV action.

Drive it from [`useStreamSpeech()`](https://localmode.dev/docs/react) (`isSynthesizing`, `isPlaying`, `currentClause`, `clauses`) and wire `onDownload` to `downloadBlob`.

**When to use it:** give live feedback during clause-by-clause Kokoro streaming and offer a one-click WAV export.

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';
import {
  StreamingSpeechPanel,
  type StreamingClause,
} from '@/components/streaming-speech-panel';

/**
 * Demo for {@link StreamingSpeechPanel}. Simulates a streaming TTS run by
 * advancing clauses on a timer so the active → finished transition is visible
 * without a model download. The real app passes `useStreamSpeech()` state and
 * wires `onDownload` to `downloadBlob`.
 */
const SAMPLE: StreamingClause[] = [
  { text: 'Welcome to LocalMode.', clauseIndex: 0 },
  { text: 'Everything runs in your browser.', clauseIndex: 1 },
  { text: 'No servers, no API keys.', clauseIndex: 2 },
];

export default function StreamingSpeechPanelDemo() {
  const [clauses, setClauses] = useState<StreamingClause[]>([]);
  const [current, setCurrent] = useState<StreamingClause | null>(null);
  const [phase, setPhase] = useState<'idle' | 'streaming' | 'done'>('idle');

  const run = () => {
    if (phase === 'streaming') return;
    setClauses([]);
    setCurrent(null);
    setPhase('streaming');

    SAMPLE.forEach((clause, i) => {
      setTimeout(() => {
        setClauses((prev) => [...prev, clause]);
        setCurrent(clause);
      }, 700 * (i + 1));
    });
    setTimeout(() => {
      setCurrent(null);
      setPhase('done');
    }, 700 * (SAMPLE.length + 1));
  };

  return (
    <div className="flex flex-col gap-4">
      <button
        type="button"
        onClick={run}
        className="inline-flex h-9 w-fit items-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90"
      >
        Simulate stream
      </button>

      <StreamingSpeechPanel
        isSynthesizing={phase === 'streaming'}
        isPlaying={phase === 'streaming'}
        currentClause={current}
        clauses={clauses}
        onDownload={() => alert('downloadBlob(wav, "speech.wav") would run here')}
      />
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/audio/streaming-speech-panel
```

Data source & dependencies [#data-source--dependencies]

**Data source:** renders the streaming-TTS state you pass and emits `onDownload` — works with any backend. Recommended producer: `useStreamSpeech` (with the Kokoro TTS model from `@localmode/transformers`) from `@localmode/react` (on-device, optional); wire `onDownload` to your own download handler (e.g. the `downloadBlob` helper).

* `clsx` + `tailwind-merge` — via the shared `cn()` util

Files installed [#files-installed]

* `streaming-speech-panel.tsx` — the component
* `waveform-activity-bars.tsx` — the active-state indicator (registry dependency)
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**StreamingSpeechPanel**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `isSynthesizing` | `boolean` | — | **Required.** True while clauses are being synthesized. |
| `isPlaying` | `boolean` | — | **Required.** True while synthesized clauses are playing. |
| `currentClause` | `object \| null` | — | **Required.** The clause currently being played, or `null`. |
| `clauses` | `array` | — | **Required.** All clauses observed so far during the active stream. |
| `onDownload` | `function` | — | Fired when the user clicks "Download WAV" in the finished state. Wire this to `downloadBlob(wavBlob, 'speech.wav')`. Omit to hide the download action. |

Examples [#examples]

Wired to useStreamSpeech [#wired-to-usestreamspeech]

```tsx
import { StreamingSpeechPanel } from '@/components/streaming-speech-panel';
import { useStreamSpeech, downloadBlob } from '@localmode/react';
import { transformers } from '@localmode/transformers';

export function Reader({ text }: { text: string }) {
  const speech = useStreamSpeech({
    model: transformers.textToSpeech('onnx-community/Kokoro-82M-v1.0-ONNX'),
    voice: 'af_heart',
  });

  return (
    <>
      <button onClick={() => speech.speak(text)}>Read aloud</button>
      <StreamingSpeechPanel
        isSynthesizing={speech.isSynthesizing}
        isPlaying={speech.isPlaying}
        currentClause={speech.currentClause}
        clauses={speech.clauses}
        onDownload={() => downloadBlob(wavBlob, 'speech.wav')}
      />
    </>
  );
}
```

Customization [#customization]

The panel renders three states — active, finished, and ready — driven entirely by the `isSynthesizing` / `isPlaying` / `clauses` props. The privacy note is plain text in the copied file; reword it to match your product voice. The Download action only fires `onDownload`; assemble the WAV from the streamed clauses (or capture the played audio) on your side and call `downloadBlob`.