@kuralle-syrinx/browser-client 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +24 -5
- package/src/index.ts +9 -1
- package/src/audio.test.ts +0 -130
- package/src/index.test.ts +0 -818
- package/src/jitter-buffer.test.ts +0 -275
- package/src/studio-page.test.ts +0 -69
- package/src/transport.test.ts +0 -233
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/browser-client",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"
|
|
5
|
+
"description": "Browser client for the Syrinx WebSocket audio protocol — mic capture, opus, jitter buffer, playout progress reporting, reconnect",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"voice",
|
|
8
|
+
"voice-agent",
|
|
9
|
+
"speech",
|
|
10
|
+
"syrinx",
|
|
11
|
+
"browser",
|
|
12
|
+
"microphone",
|
|
13
|
+
"opus"
|
|
14
|
+
],
|
|
6
15
|
"license": "MIT",
|
|
16
|
+
"homepage": "https://github.com/kuralle/syrinx#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/kuralle/syrinx.git",
|
|
20
|
+
"directory": "packages/browser-client"
|
|
21
|
+
},
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/kuralle/syrinx/issues"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
7
26
|
"main": "./src/index.ts",
|
|
8
27
|
"types": "./src/index.ts",
|
|
9
28
|
"dependencies": {
|
|
10
29
|
"@evan/opus": "1.0.3",
|
|
11
|
-
"@kuralle-syrinx/core": "4.
|
|
30
|
+
"@kuralle-syrinx/core": "4.3.0"
|
|
12
31
|
},
|
|
13
32
|
"devDependencies": {
|
|
14
33
|
"typescript": "^5.7.0",
|
|
15
|
-
"vite": "^6.
|
|
16
|
-
"vitest": "^2.
|
|
34
|
+
"vite": "^6.4.3",
|
|
35
|
+
"vitest": "^3.2.6"
|
|
17
36
|
},
|
|
18
37
|
"scripts": {
|
|
19
38
|
"dev": "vite --port 5173",
|
package/src/index.ts
CHANGED
|
@@ -535,7 +535,15 @@ export class SyrinxBrowserClient {
|
|
|
535
535
|
private handleJsonMessage(data: unknown): void {
|
|
536
536
|
if (typeof data !== "string") return;
|
|
537
537
|
try {
|
|
538
|
-
const
|
|
538
|
+
const parsed = JSON.parse(data) as unknown;
|
|
539
|
+
// Cloudflare Agents SDK control frames (cf_agent_state, cf_agent_mcp_servers, …) are
|
|
540
|
+
// broadcast by the Agent base class on the same socket a cf-agents `withVoice` worker
|
|
541
|
+
// uses for voice. They are not part of the Syrinx voice protocol — ignore them rather
|
|
542
|
+
// than throwing (which surfaced a spurious `error` event → a false "Error" badge).
|
|
543
|
+
if (isRecord(parsed) && typeof parsed["type"] === "string" && parsed["type"].startsWith("cf_agent")) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const message = parseStudioMessage(parsed);
|
|
539
547
|
if (message.type === "ready") {
|
|
540
548
|
if (message.sessionId !== undefined) this.currentSessionId = message.sessionId;
|
|
541
549
|
if (message.audio?.outputSampleRateHz && message.audio.outputSampleRateHz !== this.outputSampleRateHz) {
|
package/src/audio.test.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { encodeSyrinxAudioEnvelope } from "@kuralle-syrinx/core";
|
|
5
|
-
import {
|
|
6
|
-
decodeBrowserAssistantAudio,
|
|
7
|
-
encodeBrowserAudioEnvelopeFrame,
|
|
8
|
-
encodeBrowserAudioFrame,
|
|
9
|
-
float32ToPcm16,
|
|
10
|
-
pcm16FrameSampleCount,
|
|
11
|
-
resampleFloat32Linear,
|
|
12
|
-
} from "./audio.js";
|
|
13
|
-
|
|
14
|
-
describe("browser audio utilities", () => {
|
|
15
|
-
it("resamples 48 kHz microphone input to 16 kHz PCM without trusting AudioContext rate", () => {
|
|
16
|
-
const input = new Float32Array(480);
|
|
17
|
-
for (let i = 0; i < input.length; i += 1) input[i] = i / input.length;
|
|
18
|
-
|
|
19
|
-
const output = resampleFloat32Linear(input, {
|
|
20
|
-
fromSampleRateHz: 48000,
|
|
21
|
-
toSampleRateHz: 16000,
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
expect(output.length).toBe(160);
|
|
25
|
-
expect(output[0]).toBeCloseTo(0);
|
|
26
|
-
expect(output[1]).toBeCloseTo(input[3]!);
|
|
27
|
-
expect(output.at(-1)).toBeCloseTo(input[477]!);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("resamples 44.1 kHz microphone input to 16 kHz with stable frame sizing", () => {
|
|
31
|
-
const input = new Float32Array(441);
|
|
32
|
-
input.fill(0.25);
|
|
33
|
-
|
|
34
|
-
const output = resampleFloat32Linear(input, {
|
|
35
|
-
fromSampleRateHz: 44100,
|
|
36
|
-
toSampleRateHz: 16000,
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(output.length).toBe(160);
|
|
40
|
-
expect(pcm16FrameSampleCount(16000)).toBe(320);
|
|
41
|
-
expect(output.every((sample) => Math.abs(sample - 0.25) < 0.0001)).toBe(true);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("encodes browser Float32 audio as turn-scoped 16 kHz PCM16 JSON frames", () => {
|
|
45
|
-
const frame = encodeBrowserAudioFrame(new Float32Array([-2, -1, 0, 0.5, 1, 2]), {
|
|
46
|
-
fromSampleRateHz: 48000,
|
|
47
|
-
toSampleRateHz: 16000,
|
|
48
|
-
contextId: "review-turn",
|
|
49
|
-
});
|
|
50
|
-
const bytes = Buffer.from(frame.audio, "base64");
|
|
51
|
-
const pcm = new Int16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2);
|
|
52
|
-
|
|
53
|
-
expect(frame).toMatchObject({
|
|
54
|
-
type: "audio",
|
|
55
|
-
contextId: "review-turn",
|
|
56
|
-
sequence: undefined,
|
|
57
|
-
sampleRateHz: 16000,
|
|
58
|
-
});
|
|
59
|
-
expect(Array.from(pcm)).toEqual([-32768, 16384]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("preserves sequence metadata on encoded browser JSON audio frames", () => {
|
|
63
|
-
const frame = encodeBrowserAudioFrame(new Float32Array([0, 0.5, 1]), {
|
|
64
|
-
fromSampleRateHz: 48000,
|
|
65
|
-
toSampleRateHz: 16000,
|
|
66
|
-
contextId: "review-turn",
|
|
67
|
-
sequence: 17,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(frame).toMatchObject({
|
|
71
|
-
type: "audio",
|
|
72
|
-
contextId: "review-turn",
|
|
73
|
-
sampleRateHz: 16000,
|
|
74
|
-
sequence: 17,
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("encodes browser Float32 audio as turn-scoped binary envelope frames", () => {
|
|
79
|
-
const frame = encodeBrowserAudioEnvelopeFrame(new Float32Array([-2, -1, 0, 0.5, 1, 2]), {
|
|
80
|
-
fromSampleRateHz: 48000,
|
|
81
|
-
toSampleRateHz: 16000,
|
|
82
|
-
contextId: "review-turn",
|
|
83
|
-
sequence: 9,
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const decoded = decodeBrowserAssistantAudio(frame);
|
|
87
|
-
const bytes = new Uint8Array(decoded.data);
|
|
88
|
-
const pcm = new Int16Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 2);
|
|
89
|
-
|
|
90
|
-
expect(decoded.metadata).toMatchObject({
|
|
91
|
-
type: "audio",
|
|
92
|
-
contextId: "review-turn",
|
|
93
|
-
sampleRateHz: 16000,
|
|
94
|
-
sequence: 9,
|
|
95
|
-
encoding: "pcm_s16le",
|
|
96
|
-
channels: 1,
|
|
97
|
-
byteLength: 4,
|
|
98
|
-
});
|
|
99
|
-
expect(Array.from(pcm)).toEqual([-32768, 16384]);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("clamps Float32 samples before PCM16 conversion", () => {
|
|
103
|
-
expect(Array.from(float32ToPcm16(new Float32Array([-1.5, -1, 0, 1, 1.5])))).toEqual([
|
|
104
|
-
-32768,
|
|
105
|
-
-32768,
|
|
106
|
-
0,
|
|
107
|
-
32767,
|
|
108
|
-
32767,
|
|
109
|
-
]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("decodes enveloped assistant audio before playback", () => {
|
|
113
|
-
const encoded = encodeSyrinxAudioEnvelope({
|
|
114
|
-
type: "audio",
|
|
115
|
-
contextId: "turn-tts",
|
|
116
|
-
sampleRateHz: 16000,
|
|
117
|
-
sequence: 2,
|
|
118
|
-
byteLength: 4,
|
|
119
|
-
}, new Uint8Array([1, 2, 3, 4]));
|
|
120
|
-
|
|
121
|
-
const decoded = decodeBrowserAssistantAudio(encoded);
|
|
122
|
-
|
|
123
|
-
expect(new Uint8Array(decoded.data)).toEqual(new Uint8Array([1, 2, 3, 4]));
|
|
124
|
-
expect(decoded.metadata).toMatchObject({
|
|
125
|
-
contextId: "turn-tts",
|
|
126
|
-
sampleRateHz: 16000,
|
|
127
|
-
sequence: 2,
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|