feat: voice dictation (STT wave 1 — Deepgram cloud)
Two-band architecture from docs/research/stt.md: renderer captures mic audio in an AudioWorklet (linear16 16kHz mono, ~128ms frames, per-quantum RMS for the meter) and streams it directly to Deepgram over WebSocket — audio never loops through our server. The real key stays server-side; /api/stt/token mints a short-lived grant token (TTL 300s) per run. Patterns ported from the owner's pi dictate extension (tuned in real use): generation counter guarding every async handler, the latest-RMS-never-resets meter (-50..-10dB window, 60ms tick), mic-first stop + CloseStream + 3s safety net, endpointing=300 with the utterance_end_ms=1000 backstop. UI (owner placement): mic button in the stats bar's middle slot on all three surfaces (home graph-view, chat, draft); while recording the meter unfurls to fill the row's free width with the interim ghost at its flank. Esc ends dictation from the composer. Token mint comes before getUserMedia so a keyless app fails fast without a TCC prompt; one automatic reconnect on abnormal socket close, errors surface inline. Live-smoked the full error path in the dev shell (bar renders on all three pages, 403 grant failure shows inline, mic untouched). The full audio path needs a Deepgram key with Member permission to mint tokens.
This commit is contained in:
@@ -33,6 +33,7 @@ export function StatsBar({
|
||||
settings,
|
||||
onSettingsSaved,
|
||||
leftSlot,
|
||||
dictationSlot,
|
||||
}: {
|
||||
turns: TurnUsage[];
|
||||
chatId: string;
|
||||
@@ -41,6 +42,11 @@ export function StatsBar({
|
||||
/** Pre-chat only: replaces the (empty) session-data summary — home and
|
||||
* the /chat/new draft mount the knowledge-context slider here. */
|
||||
leftSlot?: ReactNode;
|
||||
/** The dictation strip (STT wave, 2026-09-17): the row's middle is dead
|
||||
* space between the session data and the model control — the mic
|
||||
* button lives there, and while recording the meter unfurls to fill
|
||||
* whatever width the row has (owner placement). */
|
||||
dictationSlot?: ReactNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
// Same dropdown discipline as the model control (owner, df4433f treatment,
|
||||
@@ -155,6 +161,12 @@ export function StatsBar({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* The middle slot: dictation (owner, 2026-09-17). flex-1 lets the
|
||||
meter expand into the row's free width; justify-center keeps the
|
||||
lone idle mic button visually centered. */}
|
||||
{dictationSlot && (
|
||||
<div className="flex min-w-0 flex-1 items-center justify-center">{dictationSlot}</div>
|
||||
)}
|
||||
{/* The model + thinking control — one controller for the conversation
|
||||
agents (prober/teacher); the record's `turns` above still folds
|
||||
the ACTUAL models billed, regardless of what the picker says. */}
|
||||
|
||||
Reference in New Issue
Block a user