feat(dictate): add voice-reactive listening waveform

This commit is contained in:
jb
2026-08-26 13:40:47 -04:00
parent e234468a73
commit d1a811a5c5
16 changed files with 383 additions and 73 deletions
+43 -2
View File
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
import { createConnection } from "node:net";
export const DICTATE_SOCKET_PATH = "/run/user/1000/pi-dictate.sock";
export const PROTOCOL_VERSION = 1;
export const PROTOCOL_VERSION = 2;
export const MAX_FRAME_BYTES = 64 * 1024;
export const MAX_TRANSCRIPT_BYTES = 32 * 1024;
export const MAX_ID_BYTES = 128;
@@ -10,6 +10,8 @@ export const MAX_ID_BYTES = 128;
const STATUS_KEY = "dictate";
const MAX_ERROR_DETAIL_BYTES = 2 * 1024;
const UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
const WAVEFORM_BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const;
const WAVEFORM_SAMPLES = 12;
type DictateState =
| "idle"
@@ -97,6 +99,7 @@ export function createDictateExtension(options: DictateExtensionOptions = {}) {
let receiveBuffer = Buffer.alloc(0);
let nextId = 0;
let resultDelivered = false;
let audioLevels: number[] = [];
const newId = (kind: string) => `${kind}-${++nextId}`;
@@ -104,6 +107,25 @@ export function createDictateExtension(options: DictateExtensionOptions = {}) {
activeContext?.ui.setStatus(STATUS_KEY, status);
};
const setListeningStatus = () => {
if (!activeContext) return;
const theme = activeContext.ui.theme;
const waveform = audioLevels
.map(
(level) =>
WAVEFORM_BARS[
Math.min(
WAVEFORM_BARS.length - 1,
Math.floor(level * WAVEFORM_BARS.length),
)
],
)
.join("");
setStatus(
`${theme.fg("error", "●")} ${theme.fg("dim", "listening")} ${theme.fg("accent", waveform)}`,
);
};
const closeSocket = (current: DictateSocket, force: boolean) => {
try {
if (force) current.destroy();
@@ -121,6 +143,7 @@ export function createDictateExtension(options: DictateExtensionOptions = {}) {
negotiated = false;
receiveBuffer = Buffer.alloc(0);
resultDelivered = false;
audioLevels = [];
state = "idle";
setStatus(undefined);
activeContext = null;
@@ -183,7 +206,8 @@ export function createDictateExtension(options: DictateExtensionOptions = {}) {
}
if (serviceState === "listening" && state === "connecting") {
state = "recording";
setStatus("listening...");
audioLevels = Array<number>(WAVEFORM_SAMPLES).fill(0);
setListeningStatus();
return true;
}
if (serviceState === "stopping" && state === "stopping") {
@@ -267,6 +291,22 @@ export function createDictateExtension(options: DictateExtensionOptions = {}) {
return;
}
if (value.type === "level") {
if (
state !== "recording" ||
typeof value.level !== "number" ||
!Number.isFinite(value.level) ||
value.level < 0 ||
value.level > 1
) {
protocolError("audio level is invalid for the current state.");
return;
}
audioLevels = [...audioLevels.slice(1), value.level];
setListeningStatus();
return;
}
if (value.type === "result") {
if (state !== "transcribing" || typeof value.transcript !== "string") {
protocolError("result is invalid for the current state.");
@@ -349,6 +389,7 @@ export function createDictateExtension(options: DictateExtensionOptions = {}) {
helloRequestId = newId("hello");
negotiated = false;
resultDelivered = false;
audioLevels = [];
receiveBuffer = Buffer.alloc(0);
let current: DictateSocket;