feat(transcription): add shared voice input support (#4232)
* feat(webui): add voice transcription input * feat(webui): render ANSI output in code blocks * refactor(webui): isolate voice recorder logic * refactor(transcription): keep websocket ingress thin * refactor(transcription): resolve channel audio settings on demand * style(webui): neutralize voice waveform color * feat(webui): add voice input tooltip * feat(webui): add voice input keyboard shortcut * fix(webui): distinguish voice shortcut platforms * fix(webui): place voice button after model selector * refactor(webui): share voice hold recording helpers * fix(desktop): allow microphone voice input * fix(webui): stabilize token usage month labels * feat(webui): show voice input on settings overview * fix(webui): label voice capability as recognition * fix(webui): align capability overview status * refactor(webui): isolate transcription socket handling * fix(webui): soften silent voice waveform * refactor(audio): clarify transcription service location * docs(transcription): clarify audio and provider boundaries * fix(exec): reduce session output polling flake
This commit is contained in:
@@ -412,6 +412,61 @@ describe("NanobotClient", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("sends transcription requests and resolves transcription results outside chat dispatch", async () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
reconnect: false,
|
||||
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
|
||||
});
|
||||
const handler = vi.fn();
|
||||
client.onChat("chat-a", handler);
|
||||
client.connect();
|
||||
lastSocket().fakeOpen();
|
||||
|
||||
const promise = client.transcribeAudio("data:audio/webm;base64,AAAA", {
|
||||
durationMs: 1234,
|
||||
timeoutMs: 1_000,
|
||||
});
|
||||
const frame = JSON.parse(lastSocket().sent.at(-1) as string);
|
||||
expect(frame).toMatchObject({
|
||||
type: "transcribe_audio",
|
||||
data_url: "data:audio/webm;base64,AAAA",
|
||||
duration_ms: 1234,
|
||||
});
|
||||
expect(typeof frame.request_id).toBe("string");
|
||||
|
||||
lastSocket().fakeMessage({
|
||||
event: "transcription_result",
|
||||
request_id: frame.request_id,
|
||||
text: "hello from voice",
|
||||
});
|
||||
await expect(promise).resolves.toBe("hello from voice");
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects pending transcription requests on server errors and socket close", async () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
reconnect: false,
|
||||
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
|
||||
});
|
||||
client.connect();
|
||||
lastSocket().fakeOpen();
|
||||
|
||||
const errored = client.transcribeAudio("data:audio/webm;base64,AAAA", { timeoutMs: 1_000 });
|
||||
const errorFrame = JSON.parse(lastSocket().sent.at(-1) as string);
|
||||
lastSocket().fakeMessage({
|
||||
event: "transcription_error",
|
||||
request_id: errorFrame.request_id,
|
||||
detail: "not_configured",
|
||||
});
|
||||
await expect(errored).rejects.toThrow("not_configured");
|
||||
|
||||
const dropped = client.transcribeAudio("data:audio/webm;base64,BBBB", { timeoutMs: 1_000 });
|
||||
lastSocket().close();
|
||||
await expect(dropped).rejects.toThrow("socket closed");
|
||||
});
|
||||
|
||||
it("queues sends while connecting and flushes on open", () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
|
||||
Reference in New Issue
Block a user